diff --git a/.github/workflows/ci-testing.yml b/.github/workflows/ci-testing.yml index 5375a5c0b..07c378cf4 100644 --- a/.github/workflows/ci-testing.yml +++ b/.github/workflows/ci-testing.yml @@ -15,9 +15,10 @@ jobs: node-version: '18.x' - run: sudo apt-get install xvfb - run: npm install --legacy-peer-deps + - run: npx playwright install - run: npm install -g grunt-cli - run: grunt default - run: xvfb-run --auto-servernum -- npm test - - run: xvfb-run --auto-servernum -- npm run jest +# - run: xvfb-run --auto-servernum -- npm run jest env: CI: true \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 27fcf1c90..7b89c3847 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.11.0", "license": "W3C", "devDependencies": { - "@playwright/test": "^1.24.2", + "@playwright/test": "^1.35.1", "diff": "^5.1.0", "express": "^4.17.1", "grunt": "^1.4.0", @@ -30,7 +30,7 @@ "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.79.0", "path": "^0.12.7", - "playwright": "^1.24.2", + "playwright": "^1.35.1", "proj4": "^2.6.2", "proj4leaflet": "^1.0.2", "rollup": "^2.23.1" @@ -1513,19 +1513,22 @@ } }, "node_modules/@playwright/test": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.24.2.tgz", - "integrity": "sha512-Q4X224pRHw4Dtkk5PoNJplZCokLNvVbXD9wDQEMrHcEuvWpJWEQDeJ9gEwkZ3iCWSFSWBshIX177B231XW4wOQ==", + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", + "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", "dev": true, "dependencies": { "@types/node": "*", - "playwright-core": "1.24.2" + "playwright-core": "1.35.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" } }, "node_modules/@sideway/address": { @@ -1538,9 +1541,9 @@ } }, "node_modules/@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", "dev": true }, "node_modules/@sideway/pinpoint": { @@ -4192,6 +4195,20 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -7887,9 +7904,9 @@ } }, "node_modules/jest-runner/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "peer": true, "dependencies": { @@ -8142,9 +8159,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -9054,9 +9071,9 @@ } }, "node_modules/node-notifier/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "optional": true, "dependencies": { @@ -9128,9 +9145,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -9723,31 +9740,31 @@ } }, "node_modules/playwright": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.24.2.tgz", - "integrity": "sha512-iMWDLgaFRT+7dXsNeYwgl8nhLHsUrzFyaRVC+ftr++P1dVs70mPrFKBZrGp1fOKigHV9d1syC03IpPbqLKlPsg==", + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.35.1.tgz", + "integrity": "sha512-NbwBeGJLu5m7VGM0+xtlmLAH9VUfWwYOhUi/lSEDyGg46r1CA9RWlvoc5yywxR9AzQb0mOCm7bWtOXV7/w43ZA==", "dev": true, "hasInstallScript": true, "dependencies": { - "playwright-core": "1.24.2" + "playwright-core": "1.35.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/playwright-core": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.24.2.tgz", - "integrity": "sha512-zfAoDoPY/0sDLsgSgLZwWmSCevIg1ym7CppBwllguVBNiHeixZkc1AdMuYUPZC6AdEYc4CxWEyLMBTw2YcmRrA==", + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", + "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", "dev": true, "bin": { - "playwright": "cli.js" + "playwright-core": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/posix-character-classes": { @@ -10237,6 +10254,21 @@ "fsevents": "~2.1.2" } }, + "node_modules/rollup/node_modules/fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "deprecated": "\"Please update to latest v2.3 or v2.2\"", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -10525,9 +10557,9 @@ } }, "node_modules/sane/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -10580,9 +10612,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -12117,9 +12149,9 @@ "dev": true }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -13414,13 +13446,14 @@ } }, "@playwright/test": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.24.2.tgz", - "integrity": "sha512-Q4X224pRHw4Dtkk5PoNJplZCokLNvVbXD9wDQEMrHcEuvWpJWEQDeJ9gEwkZ3iCWSFSWBshIX177B231XW4wOQ==", + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", + "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", "dev": true, "requires": { "@types/node": "*", - "playwright-core": "1.24.2" + "fsevents": "2.3.2", + "playwright-core": "1.35.1" } }, "@sideway/address": { @@ -13433,9 +13466,9 @@ } }, "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", "dev": true }, "@sideway/pinpoint": { @@ -15536,6 +15569,13 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -18360,9 +18400,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "peer": true, "requires": { @@ -18556,9 +18596,9 @@ "dev": true }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -19262,9 +19302,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "optional": true, "requires": { @@ -19320,9 +19360,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -19770,18 +19810,18 @@ } }, "playwright": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.24.2.tgz", - "integrity": "sha512-iMWDLgaFRT+7dXsNeYwgl8nhLHsUrzFyaRVC+ftr++P1dVs70mPrFKBZrGp1fOKigHV9d1syC03IpPbqLKlPsg==", + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.35.1.tgz", + "integrity": "sha512-NbwBeGJLu5m7VGM0+xtlmLAH9VUfWwYOhUi/lSEDyGg46r1CA9RWlvoc5yywxR9AzQb0mOCm7bWtOXV7/w43ZA==", "dev": true, "requires": { - "playwright-core": "1.24.2" + "playwright-core": "1.35.1" } }, "playwright-core": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.24.2.tgz", - "integrity": "sha512-zfAoDoPY/0sDLsgSgLZwWmSCevIg1ym7CppBwllguVBNiHeixZkc1AdMuYUPZC6AdEYc4CxWEyLMBTw2YcmRrA==", + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", + "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", "dev": true }, "posix-character-classes": { @@ -20151,6 +20191,15 @@ "dev": true, "requires": { "fsevents": "~2.1.2" + }, + "dependencies": { + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + } } }, "rsvp": { @@ -20389,9 +20438,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "shebang-command": { @@ -20431,9 +20480,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "send": { @@ -21662,9 +21711,9 @@ "dev": true }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, "wrap-ansi": { diff --git a/package.json b/package.json index d9aca5692..717ff2fbf 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,8 @@ "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.79.0", "path": "^0.12.7", - "@playwright/test": "^1.24.2", - "playwright": "^1.24.2", + "@playwright/test": "^1.35.1", + "playwright": "^1.35.1", "proj4": "^2.6.2", "proj4leaflet": "^1.0.2", "rollup": "^2.23.1" diff --git a/src/layer.js b/src/layer.js index 2e5f0e428..e792f090d 100644 --- a/src/layer.js +++ b/src/layer.js @@ -15,7 +15,8 @@ export class MapLayer extends HTMLElement { } } get label() { - return this.hasAttribute('label') ? this.getAttribute('label') : ''; + if (this._layer) return this._layer.getName(); + else return this.hasAttribute('label') ? this.getAttribute('label') : ''; } set label(val) { if (val) { @@ -47,12 +48,13 @@ export class MapLayer extends HTMLElement { } get opacity() { - return this._layer._container.style.opacity || this._layer.options.opacity; + // use ?? since 0 is falsy, || would return rhs in that case + return this._opacity ?? this.getAttribute('opacity'); } set opacity(val) { if (+val > 1 || +val < 0) return; - this._layer.changeOpacity(val); + this.setAttribute('opacity', val); } constructor() { @@ -72,14 +74,18 @@ export class MapLayer extends HTMLElement { } _onRemove() { - this._removeEvents(); - if (this._layer._map) { + if (this._layer) { + this._layer.off(); + } + // if this layer has never been connected, it will not have a _layer + if (this._layer && this._layer._map) { this._layer._map.removeLayer(this._layer); } if (this._layerControl && !this.hidden) { this._layerControl.removeLayer(this._layer); } + delete this._layer; if (this.shadowRoot) { this.shadowRoot.innerHTML = ''; @@ -88,41 +94,175 @@ export class MapLayer extends HTMLElement { connectedCallback() { if (this.hasAttribute('data-moving')) return; - this._onAdd(); + const doConnected = this._onAdd.bind(this); + this.parentElement + .whenReady() + .then(() => { + doConnected(); + }) + .catch(() => { + throw new Error('Map never became ready'); + }); } _onAdd() { if (this.getAttribute('src') && !this.shadowRoot) { this.attachShadow({ mode: 'open' }); } - //creates listener that waits for createmap event, this allows for delayed builds of maps - //this allows a safeguard for the case where loading a custom TCRS takes longer than loading mapml-viewer.js/web-map.js - this.parentNode.addEventListener( - 'createmap', - () => { - this._ready(); - // if the map has been attached, set this layer up wrt Leaflet map - if (this.parentNode._map) { - this._attachedToMap(); - } - if (this._layerControl && !this.hidden) { - this._layerControl.addOrUpdateOverlay(this._layer, this.label); + new Promise((resolve, reject) => { + this.addEventListener( + 'changestyle', + function (e) { + e.stopPropagation(); + this.src = e.detail.src; + }, + { once: true } + ); + this.addEventListener( + 'changeprojection', + function (e) { + e.stopPropagation(); + reject(e); + }, + { once: true } + ); + let base = this.baseURI ? this.baseURI : document.baseURI; + + const headers = new Headers(); + headers.append('Accept', 'text/mapml'); + if (this.src) { + fetch(this.src, { headers: headers }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.text(); + }) + .then((mapml) => { + let content = new DOMParser().parseFromString(mapml, 'text/xml'); + if ( + content.querySelector('parsererror') || + !content.querySelector('mapml-') + ) { + throw new Error('Parser error'); + } + if (this._layer) { + this._onRemove(); + } + this._layer = M.mapMLLayer( + new URL(this.src, base).href, + this, + content, + { + mapprojection: this.parentElement.projection, + opacity: this.opacity + } + ); + this._attachedToMap(); + this._validateDisabled(); + resolve(); + }) + .catch((error) => { + console.log('Error fetching layer content' + error); + }); + } else { + if (this._layer) { + this._onRemove(); } - }, - { once: true } - ); //listener stops listening after event occurs once - //if map is already created then dispatch createmap event, allowing layer to be built - if (this.parentNode._map) - this.parentNode.dispatchEvent(new CustomEvent('createmap')); + this._layer = M.mapMLLayer(null, this, null, { + mapprojection: this.parentElement.projection, + opacity: this.opacity + }); + this._attachedToMap(); + this._validateDisabled(); + resolve(); + } + }).catch((e) => { + if (e.type === 'changeprojection') { + this.src = e.detail.href; + } else { + console.log(e); + this.dispatchEvent( + new CustomEvent('error', { detail: { target: this } }) + ); + } + }); } + _attachedToMap() { + // set i to the position of this layer element in the set of layers + var i = 0, + position = 1; + for (var nodes = this.parentNode.children; i < nodes.length; i++) { + if (this.parentNode.children[i].nodeName === 'LAYER-') { + if (this.parentNode.children[i] === this) { + position = i + 1; + } else if (this.parentNode.children[i]._layer) { + this.parentNode.children[i]._layer.setZIndex(i + 1); + } + } + } + var proj = this.parentNode.projection + ? this.parentNode.projection + : 'OSMTILE'; + L.setOptions(this._layer, { + zIndex: position, + mapprojection: proj, + opacity: window.getComputedStyle(this).opacity + }); + // make sure the Leaflet layer has a reference to the map + this._layer._map = this.parentNode._map; + // notify the layer that it is attached to a map (layer._map) + this._layer.fire('attached'); + + if (this.checked) { + this._layer.addTo(this._layer._map); + } + + // add the handler which toggles the 'checked' property based on the + // user checking/unchecking the layer from the layer control + // this must be done *after* the layer is actually added to the map + this._layer.on('add remove', this._onLayerChange, this); + this._layer.on('add remove', this._validateDisabled, this); + // toggle the this.disabled attribute depending on whether the layer + // is: same prj as map, within view/zoom of map + this._layer._map.on('moveend layeradd', this._validateDisabled, this); - adoptedCallback() { - // console.log('Custom map element moved to new page.'); + // if controls option is enabled, insert the layer into the overlays array + if (this.parentNode._layerControl && !this.hidden) { + this._layerControl = this.parentNode._layerControl; + this._layerControl.addOrUpdateOverlay(this._layer, this.label); + } + + // the mapml document associated to this layer can in theory contain many + // link[@rel=legend] elements with different @type or other attributes; + // currently only support a single link, don't care about type, lang etc. + // TODO: add support for full LayerLegend object, and > one link. + if (this._layer._legendUrl) { + this.legendLinks = [ + { + type: 'application/octet-stream', + href: this._layer._legendUrl, + rel: 'legend', + lang: null, + hreflang: null, + sizes: null + } + ]; + } + // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied + // to MapML extent as metadata + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event + this.dispatchEvent( + new CustomEvent('loadedmetadata', { detail: { target: this } }) + ); } + attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'label': - this?._layer?.setName(newValue); + this.whenReady().then(() => { + this._layer.setName(newValue); + }); break; case 'checked': if (this._layer) { @@ -150,71 +290,21 @@ export class MapLayer extends HTMLElement { break; case 'opacity': if (oldValue !== newValue && this._layer) { - this.opacity = newValue; + this._opacity = newValue; + this._layer.changeOpacity(newValue); } break; case 'src': if (oldValue !== newValue && this._layer) { - this._reload(); + this._onRemove(); + if (this.isConnected) { + this._onAdd(); + } // the original inline content will not be removed // but has NO EFFECT and works as a fallback } } } - // re-load the layer element when the src attribute is changed - _reload() { - let oldOpacity = this.opacity; - // go through the same sequence as if the layer had been removed from - // the DOM and re-attached with a new URL source. - this._onRemove(); - if (this.isConnected) { - this._onAdd(); - } - this.opacity = oldOpacity; - } - _onLayerExtentLoad(e) { - // the mapml document associated to this layer can in theory contain many - // link[@rel=legend] elements with different @type or other attributes; - // currently only support a single link, don't care about type, lang etc. - // TODO: add support for full LayerLegend object, and > one link. - if (this._layer._legendUrl) { - this.legendLinks = [ - { - type: 'application/octet-stream', - href: this._layer._legendUrl, - rel: 'legend', - lang: null, - hreflang: null, - sizes: null - } - ]; - } - if (this._layer._title) { - this.label = this._layer._title; - } - // make sure local content layer has the chance to set its extent properly - // which is important for the layer control and the disabled property - if (this._layer._map) { - this._layer.fire('attached', this._layer); - } - // TODO ensure the controls in this._layerControl contain 'live' controls - // which control the layer, not potentially the previous style / src - if (this._layerControl) { - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - } - if (!this._layer.error) { - // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied - // to MapML extent as metadata - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event - this.dispatchEvent( - new CustomEvent('loadedmetadata', { detail: { target: this } }) - ); - } else { - this.dispatchEvent( - new CustomEvent('error', { detail: { target: this } }) - ); - } - } _validateDisabled() { setTimeout(() => { let layer = this._layer, @@ -233,29 +323,34 @@ export class MapLayer extends HTMLElement { let type = layerTypes[j]; if (this.checked && layer[type]) { if (type === '_templatedLayer') { - for (let i = 0; i < layer._extent._mapExtents.length; i++) { + for (let i = 0; i < layer._properties._mapExtents.length; i++) { for ( let j = 0; j < - layer._extent._mapExtents[i].templatedLayer._templates + layer._properties._mapExtents[i].templatedLayer._templates .length; j++ ) { if ( - layer._extent._mapExtents[i].templatedLayer._templates[j] - .rel === 'query' + layer._properties._mapExtents[i].templatedLayer + ._templates[j].rel === 'query' ) continue; total++; - layer._extent._mapExtents[i].removeAttribute('disabled'); - layer._extent._mapExtents[i].disabled = false; + layer._properties._mapExtents[i].removeAttribute( + 'disabled' + ); + layer._properties._mapExtents[i].disabled = false; if ( - !layer._extent._mapExtents[i].templatedLayer._templates[j] - .layer.isVisible + !layer._properties._mapExtents[i].templatedLayer + ._templates[j].layer.isVisible ) { count++; - layer._extent._mapExtents[i].setAttribute('disabled', ''); - layer._extent._mapExtents[i].disabled = true; + layer._properties._mapExtents[i].setAttribute( + 'disabled', + '' + ); + layer._properties._mapExtents[i].disabled = true; } } } @@ -332,119 +427,9 @@ export class MapLayer extends HTMLElement { this.checked = this._layer._map.hasLayer(this._layer); } } - _ready() { - // the layer might not be attached to a map - // so we need a way for non-src based layers to establish what their - // zoom range, extent and projection are. meta elements in content to - // allow the author to provide this explicitly are one way, they will - // be parsed from the second parameter here - // IE 11 did not have a value for this.baseURI for some reason - var base = this.baseURI ? this.baseURI : document.baseURI; - let opacity_value = this.hasAttribute('opacity') - ? this.getAttribute('opacity') - : '1.0'; - this._layer = M.mapMLLayer( - this.src ? new URL(this.src, base).href : null, - this, - { - mapprojection: this.parentElement._map.options.projection, - opacity: opacity_value - } - ); - this._layer.on('extentload', this._onLayerExtentLoad, this); - this._setUpEvents(); - } - _attachedToMap() { - // set i to the position of this layer element in the set of layers - var i = 0, - position = 1; - for (var nodes = this.parentNode.children; i < nodes.length; i++) { - if (this.parentNode.children[i].nodeName === 'LAYER-') { - if (this.parentNode.children[i] === this) { - position = i + 1; - } else if (this.parentNode.children[i]._layer) { - this.parentNode.children[i]._layer.setZIndex(i + 1); - } - } - } - var proj = this.parentNode.projection - ? this.parentNode.projection - : 'OSMTILE'; - L.setOptions(this._layer, { - zIndex: position, - mapprojection: proj, - opacity: window.getComputedStyle(this).opacity - }); - // make sure the Leaflet layer has a reference to the map - this._layer._map = this.parentNode._map; - // notify the layer that it is attached to a map (layer._map) - this._layer.fire('attached'); - - if (this.checked) { - this._layer.addTo(this._layer._map); - } - - // add the handler which toggles the 'checked' property based on the - // user checking/unchecking the layer from the layer control - // this must be done *after* the layer is actually added to the map - this._layer.on('add remove', this._onLayerChange, this); - this._layer.on('add remove extentload', this._validateDisabled, this); - - // if controls option is enabled, insert the layer into the overlays array - if (this.parentNode._layerControl && !this.hidden) { - this._layerControl = this.parentNode._layerControl; - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - } - // toggle the this.disabled attribute depending on whether the layer - // is: same prj as map, within view/zoom of map - this._layer._map.on('moveend', this._validateDisabled, this); - this._layer._map.on('checkdisabled', this._validateDisabled, this); - // this is necessary to get the layer control to compare the layer - // extents with the map extent & zoom, but it needs to be rethought TODO - // for one thing, layers which are checked by the author before - // adding to the map are displayed despite that they are not visible - // See issue #26 - // this._layer._map.fire('moveend'); - } - _removeEvents() { - if (this._layer) { - this._layer.off(); - } - } - _setUpEvents() { - this._layer.on( - 'loadstart', - function () { - this.dispatchEvent( - new CustomEvent('loadstart', { detail: { target: this } }) - ); - }, - this - ); - this._layer.on( - 'changestyle', - function (e) { - this.src = e.src; - this.dispatchEvent( - new CustomEvent('changestyle', { detail: { target: this } }) - ); - }, - this - ); - this._layer.on( - 'changeprojection', - function (e) { - this.src = e.href; - this.dispatchEvent( - new CustomEvent('changeprojection', { detail: { target: this } }) - ); - }, - this - ); - } zoomTo() { if (!this.extent) return; - let map = this._layer._map, + let map = this.parentElement._map, tL = this.extent.topLeft.pcrs, bR = this.extent.bottomRight.pcrs, layerBounds = L.bounds( @@ -479,4 +464,28 @@ export class MapLayer extends HTMLElement { } } } + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this._layer) { + resolve(); + } else { + let layerElement = this; + interval = setInterval(testForLayer, 200, layerElement); + failureTimer = setTimeout(layerNotDefined, 5000); + } + function testForLayer(layerElement) { + if (layerElement._layer) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function layerNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for layer to be ready'); + } + }); + } } diff --git a/src/map-area.js b/src/map-area.js index a50c32af4..ae436f9d0 100644 --- a/src/map-area.js +++ b/src/map-area.js @@ -60,10 +60,9 @@ export class MapArea extends HTMLAreaElement { } attributeChangedCallback(name, oldValue, newValue) {} connectedCallback() { - // if the map has been attached, set this layer up wrt Leaflet map - if (this.parentElement._map) { + this.parentElement.whenReady().then(() => { this._attachedToMap(); - } + }); } _attachedToMap() { // need the map to convert container points to LatLngs diff --git a/src/map-extent.js b/src/map-extent.js index bb81f376e..5808586a5 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -78,17 +78,38 @@ export class MapExtent extends HTMLElement { this.parentNode.nodeName.toUpperCase() === 'LAYER-' ? this.parentNode : this.parentNode.host; - if (!parentLayer._layer) { - // for custom projection cases, the MapMLLayer has not yet created and binded with the layer- at this point, - // because the "createMap" event of mapml-viewer has not yet been dispatched, the map has not yet been created - // the event will be dispatched after defineCustomProjection > projection setter - // should wait until MapMLLayer is built - parentLayer.parentNode.addEventListener('createmap', (e) => { + parentLayer + .whenReady() + .then(() => { this._layer = parentLayer._layer; + }) + .catch(() => { + throw new Error('Layer never became ready'); }); - } else { - this._layer = parentLayer._layer; - } } disconnectedCallback() {} + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this._layer) { + resolve(); + } else { + let extentElement = this; + interval = setInterval(testForExtent, 300, extentElement); + failureTimer = setTimeout(extentNotDefined, 10000); + } + function testForExtent(extentElement) { + if (extentElement._layer) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function extentNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for extent to be ready'); + } + }); + } } diff --git a/src/map-feature.js b/src/map-feature.js index f54d94f5a..032e790bc 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -72,19 +72,22 @@ export class MapFeature extends HTMLElement { case 'zoom': { if (oldValue !== newValue && this._layer) { let layer = this._layer, - layerEl = layer._layerEl, + zoom = newValue, mapmlvectors = layer._mapmlvectors; // if the vector layer only has static features, should update zoom bounds when zoom attribute is changed - if (mapmlvectors?._staticFeature) { + if (mapmlvectors._staticFeature) { this._removeInFeatureList(oldValue); + if (zoom in mapmlvectors._features) { + mapmlvectors._features[zoom].push(this._featureGroup); + } else { + mapmlvectors._features[zoom] = [this._featureGroup]; + } let native = this._getNativeZoomAndCS(layer._content); - mapmlvectors.zoomBounds = mapmlvectors._getZoomBounds( - layerEl.shadowRoot || layerEl, + mapmlvectors.zoomBounds = M.getZoomBounds( + layer._content, native.zoom ); } - this._removeFeature(); - this._updateFeature(); } break; } @@ -120,8 +123,7 @@ export class MapFeature extends HTMLElement { return; } // re-render feature if there is any observed change - this._removeFeature(); - this._updateFeature(); + this._reRender(); } }); this._observer.observe(this, { @@ -134,11 +136,31 @@ export class MapFeature extends HTMLElement { } disconnectedCallback() { + if (!this._layer) return; if (this._layer._layerEl.hasAttribute('data-moving')) return; this._removeFeature(); this._observer.disconnect(); } + _reRender() { + if (this._groupEl.isConnected) { + let native = this._getNativeZoomAndCS(this._layer._content); + let placeholder = document.createElement('span'); + this._groupEl.insertAdjacentElement('beforebegin', placeholder); + + this._featureGroup._map.removeLayer(this._featureGroup); + // Garbage collection needed + this._featureGroup = this._layer._mapmlvectors + .addData(this, native.cs, native.zoom) + .addTo(this._map); + placeholder.replaceWith(this._featureGroup.options.group); + // TODO: getBounds() should dynamically update the layerBounds and zoomBounds + this._layer._setLayerElExtent(); + delete this._getFeatureExtent; + this._setUpEvents(); + } + } + _removeFeature() { // if the el is disconnected // the el has already got removed at this point @@ -158,8 +180,8 @@ export class MapFeature extends HTMLElement { } let container = this._layer.shadowRoot || this._layer._layerEl; // update zoom bounds of vector layer - mapmlvectors.zoomBounds = mapmlvectors._getZoomBounds( - container, + mapmlvectors.zoomBounds = M.getZoomBounds( + this._layer._content, this._getNativeZoomAndCS(this._layer._content).zoom ); } @@ -180,88 +202,45 @@ export class MapFeature extends HTMLElement { ? this.parentNode : this.parentNode.host; - // arrow function is not hoisted, define before use - var _attachedToMap = (e) => { - if (!this._parentEl._layer._map) { - // if the parent layer- el has not yet added to the map (i.e. not yet rendered), wait until it is added - this._layer.once( - 'attached', - function () { - this._map = this._layer._map; - }, - this - ); - } else { - this._map = this._layer._map; - } - // "synchronize" the event handlers between map-feature and - if (!this.querySelector('map-geometry')) return; - if (!this._layer._mapmlvectors) { - // if vector layer has not yet created (i.e. the layer- is not yet rendered on the map / layer is empty) - let layerEl = this._layer._layerEl; - this._layer.once('add', this._setUpEvents, this); - if ( - !layerEl.querySelector('map-extent, map-tile') && - !layerEl.hasAttribute('src') && - layerEl.querySelectorAll('map-feature').length === 1 - ) { - // if the map-feature is added to an empty layer, fire extentload to create vector layer - // must re-run _initialize of MapMLLayer.js to re-set layer._extent (layer._extent is null for an empty layer) - this._layer._initialize(layerEl); - this._layer.fire('extentload'); - } - return; - } else if (!this._featureGroup) { - // if the map-feature el or its subtree is updated - // this._featureGroup has been free in this._removeFeature() - this._updateFeature(); - } else { - this._setUpEvents(); - } - }; - - if (!this._parentEl._layer) { - // for custom projection cases, the MapMLLayer has not yet created and binded with the layer- at this point, - // because the "createMap" event of mapml-viewer has not yet been dispatched, the map has not yet been created - // the event will be dispatched after defineCustomProjection > projection setter - // should wait until MapMLLayer is built + this._parentEl.whenReady().then(() => { let parentLayer = this._parentEl.nodeName.toUpperCase() === 'LAYER-' ? this._parentEl : this._parentEl.parentElement || this._parentEl.parentNode.host; - parentLayer.parentNode.addEventListener('createmap', (e) => { - this._layer = parentLayer._layer; - _attachedToMap(); - }); - } else { - this._layer = this._parentEl._layer; - _attachedToMap(); - } - } + this._layer = parentLayer._layer; + this._map = this._layer._map; + let mapmlvectors = this._layer._mapmlvectors; + // "synchronize" the event handlers between map-feature and + if (!this.querySelector('map-geometry')) return; + if (!this._extentEl) { + let native = this._getNativeZoomAndCS(this._layer._content); + this._featureGroup = mapmlvectors.addData(this, native.cs, native.zoom); + if (parentLayer.checked) { + this._featureGroup.addTo(this._map); + } + mapmlvectors._layers[this._featureGroup._leaflet_id] = + this._featureGroup; + if (mapmlvectors._staticFeature && !this._extentEl) { + // update zoom bounds of vector layer + mapmlvectors.zoomBounds = M.getZoomBounds( + this._layer._content, + this._getNativeZoomAndCS(this._layer._content).zoom + ); + // todo: dynamically update layer bounds of vector layer + mapmlvectors.layerBounds = M.getBounds(this._layer._content); + // update map's zoom limit + this._map._addZoomLimit(mapmlvectors); + // TODO: can be set as a handler of featureLayer + mapmlvectors._resetFeatures(); + L.extend(mapmlvectors.options, mapmlvectors.zoomBounds); + } + } - _updateFeature() { - let mapmlvectors = this._layer._mapmlvectors; - // if the parent layer has not yet rendered on the map - if (!mapmlvectors) return; - // if the is not removed, then regenerate featureGroup and update the mapmlvectors accordingly - let native = this._getNativeZoomAndCS(this._layer._content); - this._featureGroup = mapmlvectors.addData(this, native.cs, native.zoom); - mapmlvectors._layers[this._featureGroup._leaflet_id] = this._featureGroup; - this._groupEl = this._featureGroup.options.group; - if (mapmlvectors._staticFeature) { - let container = this._layer.shadowRoot || this._layer._layerEl; - // update zoom bounds of vector layer - mapmlvectors.zoomBounds = mapmlvectors._getZoomBounds( - container, - this._getNativeZoomAndCS(this._layer._content).zoom - ); - // add feature layers to map - mapmlvectors._resetFeatures(); - // update map's zoom limit - this._map._addZoomLimit(mapmlvectors); - L.extend(mapmlvectors.options, mapmlvectors.zoomBounds); - } - this._setUpEvents(); + if (Object.keys(mapmlvectors._layers).length === 1) { + this._layer._setLayerElExtent(); + } + this._setUpEvents(); + }); } _setUpEvents() { @@ -327,7 +306,7 @@ export class MapFeature extends HTMLElement { // feature attaches to layer- or layer-'s shadow if (content.nodeType === Node.DOCUMENT_NODE) { // for features migrated from mapml, read native zoom and cs from the remote mapml - return this._layer._mapmlvectors._getNativeVariables(content); + return M.getNativeVariables(content); } else if (content.nodeName.toUpperCase() === 'LAYER-') { // for inline features, read native zoom and cs from inline map-meta let zoomMeta = this._parentEl.querySelectorAll('map-meta[name=zoom]'), @@ -413,7 +392,13 @@ export class MapFeature extends HTMLElement { projection ); } - let result = M._convertAndFormatPCRS(pcrsBound, map); + let result = Object.assign( + M._convertAndFormatPCRS( + pcrsBound, + map.options.crs, + map.options.projection + ) + ); // memoize calculated result extentCache = result; return result; @@ -448,7 +433,11 @@ export class MapFeature extends HTMLElement { // find and remove the feature from mapmlvectors._features if vector layer only contains static features, helper function // prevent it from being rendered again when zooming in / out (mapmlvectors.resetFeature() is invoked) + // TODO: Can be moved to FeatureLayer.js, pass in leaflet id for layer to remove _removeInFeatureList(zoom) { + if (zoom === null) { + return; + } let mapmlvectors = this._layer._mapmlvectors; for (let i = 0; i < mapmlvectors._features[zoom].length; ++i) { let feature = mapmlvectors._features[zoom][i]; @@ -645,4 +634,30 @@ export class MapFeature extends HTMLElement { center = map.options.crs.unproject(bound.getCenter(true)); map.setView(center, this.getMaxZoom(), { animate: false }); } + whenReady() { + return this._parentEl.whenReady().then(() => { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this._featureGroup) { + resolve(); + } else { + let featureElement = this; + interval = setInterval(testForFeature, 200, featureElement); + failureTimer = setTimeout(featureNotDefined, 5000); + } + function testForFeature(featureElement) { + if (featureElement._featureGroup) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function featureNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for feature to be ready'); + } + }); + }); + } } diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 1210f4b72..4fe1e7ebe 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -76,22 +76,18 @@ export class MapViewer extends HTMLElement { get projection() { return this.hasAttribute('projection') ? this.getAttribute('projection') - : ''; + : 'OSMTILE'; } set projection(val) { - if (val && M[val]) { - this.setAttribute('projection', val); - if (this._map && this._map.options.projection !== val) { - this._map.options.crs = M[val]; - this._map.options.projection = val; - for (let layer of this.querySelectorAll('layer-')) { - layer.removeAttribute('disabled'); - let reAttach = this.removeChild(layer); - this.appendChild(reAttach); - } - if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); - } else this.dispatchEvent(new CustomEvent('createmap')); - } else throw new Error('Undefined Projection'); + if (val) { + this.whenProjectionDefined(val) + .then(() => { + this.setAttribute('projection', val); + }) + .catch(() => { + throw new Error('Undefined projection:' + val); + }); + } } get zoom() { return this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0; @@ -113,7 +109,11 @@ export class MapViewer extends HTMLElement { map.getZoom(), map.options.projection ); - let formattedExtent = M._convertAndFormatPCRS(pcrsBounds, map); + let formattedExtent = M._convertAndFormatPCRS( + pcrsBounds, + map.options.crs, + this.projection + ); if (map.getMaxZoom() !== Infinity) { formattedExtent.zoom = { minZoom: map.getMinZoom(), @@ -141,81 +141,70 @@ export class MapViewer extends HTMLElement { this._traversalCall = false; } connectedCallback() { - this._initShadowRoot(); + this.whenProjectionDefined(this.projection) + .then(() => { + this._initShadowRoot(); - this._controlsList = new DOMTokenList( - this.getAttribute('controlslist'), - this, - 'controlslist', - [ - 'noreload', - 'nofullscreen', - 'nozoom', - 'nolayer', - 'noscale', - 'geolocation' - ] - ); + this._controlsList = new DOMTokenList( + this.getAttribute('controlslist'), + this, + 'controlslist', + [ + 'noreload', + 'nofullscreen', + 'nozoom', + 'nolayer', + 'noscale', + 'geolocation' + ] + ); - var s = window.getComputedStyle(this), - wpx = s.width, - hpx = s.height, - w = this.hasAttribute('width') - ? this.getAttribute('width') - : parseInt(wpx.replace('px', '')), - h = this.hasAttribute('height') - ? this.getAttribute('height') - : parseInt(hpx.replace('px', '')); - this._changeWidth(w); - this._changeHeight(h); + var s = window.getComputedStyle(this), + wpx = s.width, + hpx = s.height, + w = this.hasAttribute('width') + ? this.getAttribute('width') + : parseInt(wpx.replace('px', '')), + h = this.hasAttribute('height') + ? this.getAttribute('height') + : parseInt(hpx.replace('px', '')); + this._changeWidth(w); + this._changeHeight(h); - // wait for createmap event before creating leaflet map - // this allows a safeguard for the case where loading a custom TCRS takes - // longer than loading mapml-viewer.js/web-map.js - // the REASON we need a synchronous event listener (see comment below) - // is because the mapml-viewer element has / can have a size of 0 up until after - // something that happens between this point and the event handler executing - // perhaps a browser rendering cycle?? - this.addEventListener('createmap', this._createMap); + this._createMap(); - let custom = !['CBMTILE', 'APSTILE', 'OSMTILE', 'WGS84'].includes( - this.projection - ); - // this is worth a read, because dispatchEvent is synchronous - // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent - // In particular: - // "All applicable event handlers are called and return before dispatchEvent() returns." - if (!custom) { - this.dispatchEvent(new CustomEvent('createmap')); - } - // https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/274 - this.setAttribute('role', 'application'); - this._toggleStatic(); + // https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/274 + this.setAttribute('role', 'application'); + this._toggleStatic(); - /* - 1. only deletes aria-label when the last (only remaining) map caption is removed - 2. only deletes aria-label if the aria-label was defined by the map caption element itself - */ + /* + 1. only deletes aria-label when the last (only remaining) map caption is removed + 2. only deletes aria-label if the aria-label was defined by the map caption element itself + */ - let mapcaption = this.querySelector('map-caption'); + let mapcaption = this.querySelector('map-caption'); - if (mapcaption !== null) { - setTimeout(() => { - let ariaupdate = this.getAttribute('aria-label'); + if (mapcaption !== null) { + setTimeout(() => { + let ariaupdate = this.getAttribute('aria-label'); - if (ariaupdate === mapcaption.innerHTML) { - this.mapCaptionObserver = new MutationObserver((m) => { - let mapcaptionupdate = this.querySelector('map-caption'); - if (mapcaptionupdate !== mapcaption) { - this.removeAttribute('aria-label'); + if (ariaupdate === mapcaption.innerHTML) { + this.mapCaptionObserver = new MutationObserver((m) => { + let mapcaptionupdate = this.querySelector('map-caption'); + if (mapcaptionupdate !== mapcaption) { + this.removeAttribute('aria-label'); + } + }); + this.mapCaptionObserver.observe(this, { + childList: true + }); } - }); - this.mapCaptionObserver.observe(this, { - childList: true - }); + }, 0); } - }, 0); - } + }) + .catch(() => { + throw new Error('Projection not defined'); + }); } _initShadowRoot() { if (!this.shadowRoot) { @@ -284,13 +273,7 @@ export class MapViewer extends HTMLElement { mapEl: this, crs: M[this.projection], zoom: this.zoom, - zoomControl: false, - // because the M.MapMLLayer invokes _tileLayer._onMoveEnd when - // the mapml response is received the screen tends to flash. I'm sure - // there is a better configuration than that, but at this moment - // I'm not sure how to approach that issue. - // See https://github.com/Maps4HTML/MapML-Leaflet-Client/issues/24 - fadeAnimation: true + zoomControl: false }); this._addToHistory(); @@ -362,6 +345,48 @@ export class MapViewer extends HTMLElement { case 'static': this._toggleStatic(); break; + case 'projection': + const reconnectLayers = () => { + if (this._map && this._map.options.projection !== newValue) { + // save map location and zoom + let lat = this.lat; + let lon = this.lon; + let zoom = this.zoom; + // saving the lat, lon and zoom is necessary because Leaflet seems + // to try to compensate for the change in the scales for each zoom + // level in the crs by changing the zoom level of the map when + // you set the map crs. So, we save the current view for use below + // when all the layers' reconnections have settled. + this._map.options.crs = M[newValue]; + this._map.options.projection = newValue; + let layersReady = []; + this._map.announceMovement.disable(); + for (let layer of this.querySelectorAll('layer-')) { + layer.removeAttribute('disabled'); + let reAttach = this.removeChild(layer); + this.appendChild(reAttach); + layersReady.push(reAttach.whenReady()); + } + Promise.allSettled(layersReady).then(() => { + // use the saved map location to ensure it is correct after + // changing the map CRS. Specifically affects projection + // upgrades, e.g. https://maps4html.org/experiments/custom-projections/BNG/ + this.zoomTo(lat, lon, zoom); + this._resetHistory(); + this._map.announceMovement.enable(); + }); + } + }; + if (newValue) { + const connect = reconnectLayers.bind(this); + new Promise((resolve, reject) => { + connect(); + resolve(); + }).then(() => { + if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); + }); + } + break; } } @@ -815,7 +840,7 @@ export class MapViewer extends HTMLElement { this._updateMapCenter(); this._addToHistory(); this.dispatchEvent( - new CustomEvent('moveend', { detail: { target: this } }) + new CustomEvent('map-moveend', { detail: { target: this } }) ); }, this @@ -936,7 +961,13 @@ export class MapViewer extends HTMLElement { this.lon = this._map.getCenter().lng; this.zoom = this._map.getZoom(); } - + _resetHistory() { + this._history = []; + this._historyIndex = -1; + this._traversalCall = false; + // weird but ok + this._addToHistory(); + } /** * Adds to the maps history on moveends * @private @@ -1087,6 +1118,7 @@ export class MapViewer extends HTMLElement { this._traversalCall = 1; this._map.panBy([initialLocation.x - curr.x, initialLocation.y - curr.y]); } + this._map.getContainer().focus(); } _toggleFullScreen() { @@ -1288,7 +1320,60 @@ export class MapViewer extends HTMLElement { M[t.projection.toUpperCase()] = M[t.projection]; //adds the projection uppercase to global M return t.projection; } - + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this._map) { + resolve(); + } else { + let viewer = this; + interval = setInterval(testForMap, 200, viewer); + failureTimer = setTimeout(mapNotDefined, 5000); + } + function testForMap(viewer) { + if (viewer._map) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function mapNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for map to be ready'); + } + }); + } + async whenLayersReady() { + let layersReady = []; + for (let layer of [...this.layers]) { + layersReady.push(layer.whenReady()); + } + return Promise.allSettled(layersReady); + } + whenProjectionDefined(projection) { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (M[projection]) { + resolve(); + } else { + interval = setInterval(testForProjection, 200, projection); + failureTimer = setTimeout(projectionNotDefined, 5000); + } + function testForProjection(p) { + if (M[p]) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function projectionNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for projection to be defined'); + } + }); + } geojson2mapml(json, options = {}) { if (options.projection === undefined) { options.projection = this.projection; diff --git a/src/mapml.css b/src/mapml.css index e6eb37a4c..eaa5990e3 100644 --- a/src/mapml.css +++ b/src/mapml.css @@ -51,7 +51,7 @@ left: 45px; } } - + /* Generic class for seamless buttons */ .mapml-button { background-color: transparent; @@ -909,7 +909,16 @@ label.mapml-layer-item-toggle { width: 450px; font-size: 16px; } - +@container leafletmap (max-width: 650px ) { + .mapml-feature-index { + width: 70cqw; + } +} +@container leafletmap (max-width: 390px ) { + .mapml-feature-index { + bottom: 100px; + } +} .mapml-feature-index-content > span{ width: 140px; white-space: nowrap; diff --git a/src/mapml/control/FullscreenButton.js b/src/mapml/control/FullscreenButton.js index 2ade90123..9b0448e93 100644 --- a/src/mapml/control/FullscreenButton.js +++ b/src/mapml/control/FullscreenButton.js @@ -84,6 +84,7 @@ L.Map.include({ this._enablePseudoFullscreen(container); } } + this.getContainer().focus(); }, _enablePseudoFullscreen: function (container) { diff --git a/src/mapml/control/GeolocationButton.js b/src/mapml/control/GeolocationButton.js index 56be01a3c..a69311735 100644 --- a/src/mapml/control/GeolocationButton.js +++ b/src/mapml/control/GeolocationButton.js @@ -4,18 +4,28 @@ export var GeolocationButton = L.Control.extend({ }, onAdd: function (map) { - this.locateControl = L.control - .locate({ - showPopup: false, - strings: { - title: M.options.locale.btnLocTrackOff - }, - position: this.options.position, - locateOptions: { - maxZoom: 16 - } - }) - .addTo(map); + // customize locate control to focus map after start/stop, so that + // featureIndexOverlay is correctly displayed + L.Control.CustomLocate = L.Control.Locate.extend({ + start: function () { + L.Control.Locate.prototype.start.call(this); + map.getContainer().focus(); + }, + stop: function () { + L.Control.Locate.prototype.stop.call(this); + map.getContainer().focus(); + } + }); + this.locateControl = new L.Control.CustomLocate({ + showPopup: false, + strings: { + title: M.options.locale.btnLocTrackOff + }, + position: this.options.position, + locateOptions: { + maxZoom: 16 + } + }).addTo(map); var container = this.locateControl._container; var button = this.locateControl; diff --git a/src/mapml/control/LayerControl.js b/src/mapml/control/LayerControl.js index bd21a3014..2f5404070 100644 --- a/src/mapml/control/LayerControl.js +++ b/src/mapml/control/LayerControl.js @@ -63,7 +63,6 @@ export var LayerControl = L.Control.Layers.extend({ // on the map it does not generate layer events for (var i = 0; i < this._layers.length; i++) { this._layers[i].layer.off('add remove', this._onLayerChange, this); - this._layers[i].layer.off('extentload', this._validateInput, this); } }, addOrUpdateOverlay: function (layer, name) { @@ -109,20 +108,20 @@ export var LayerControl = L.Control.Layers.extend({ } // check if an extent is disabled and disable it if ( - this._layers[i].layer._extent && - this._layers[i].layer._extent._mapExtents + this._layers[i].layer._properties && + this._layers[i].layer._properties._mapExtents ) { for ( let j = 0; - j < this._layers[i].layer._extent._mapExtents.length; + j < this._layers[i].layer._properties._mapExtents.length; j++ ) { let input = - this._layers[i].layer._extent._mapExtents[j].extentAnatomy, + this._layers[i].layer._properties._mapExtents[j].extentAnatomy, label = input.getElementsByClassName('mapml-layer-item-name')[0]; if ( - this._layers[i].layer._extent._mapExtents[j].disabled && - this._layers[i].layer._extent._mapExtents[j].checked + this._layers[i].layer._properties._mapExtents[j].disabled && + this._layers[i].layer._properties._mapExtents[j].checked ) { label.style.fontStyle = 'italic'; input.disabled = true; @@ -163,11 +162,6 @@ export var LayerControl = L.Control.Layers.extend({ obj.input.layerId = L.stamp(obj.layer); L.DomEvent.on(obj.input, 'click', this._onInputClick, this); - // this is necessary because when there are several layers in the - // layer control, the response to the last one can be a long time - // after the info is first displayed, so we have to go back and - // verify the layer element is not disabled and can have an enabled input. - obj.layer.on('extentload', this._validateInput, this); this._overlaysList.appendChild(layercontrols); return layercontrols; }, diff --git a/src/mapml/features/featureRenderer.js b/src/mapml/features/featureRenderer.js index 87c25d134..f04299f9d 100644 --- a/src/mapml/features/featureRenderer.js +++ b/src/mapml/features/featureRenderer.js @@ -22,8 +22,8 @@ export var FeatureRenderer = L.SVG.extend({ }, /** - * Creates all the appropriate path elements for a M.Feature - * @param {M.Feature} layer - The M.Feature that needs paths generated + * Creates all the appropriate path elements for a M.Path + * @param {M.Path} layer - The M.Path that needs paths generated * @param {boolean} stampLayer - Whether or not a layer should be stamped and stored in the renderer layers * @private */ @@ -107,7 +107,7 @@ export var FeatureRenderer = L.SVG.extend({ /** * Adds all the paths needed for a feature - * @param {M.Feature} layer - The feature that needs it's paths added + * @param {M.Path} layer - The feature that needs it's paths added * @param {HTMLElement} container - The location the paths need to be added to * @param {boolean} interactive - Whether a feature is interactive or not * @private @@ -157,7 +157,7 @@ export var FeatureRenderer = L.SVG.extend({ /** * Removes all the paths related to a feature - * @param {M.Feature} layer - The feature who's paths need to be removed + * @param {M.Path} layer - The feature who's paths need to be removed * @private */ _removePath: function (layer) { @@ -178,7 +178,7 @@ export var FeatureRenderer = L.SVG.extend({ /** * Updates the d attribute of all paths of a feature - * @param {M.Feature} layer - The Feature that needs updating + * @param {M.Path} layer - The Feature that needs updating * @private */ _updateFeature: function (layer) { @@ -212,7 +212,7 @@ export var FeatureRenderer = L.SVG.extend({ /** * Updates the styles of all paths of a feature - * @param {M.Feature} layer - The feature that needs styles updated + * @param {M.Path} layer - The feature that needs styles updated * @private */ _updateStyle: function (layer) { @@ -230,7 +230,7 @@ export var FeatureRenderer = L.SVG.extend({ /** * Updates the style of a single path * @param {HTMLElement} path - The path that needs updating - * @param {M.Feature} layer - The feature layer + * @param {M.Path} layer - The feature layer * @param {boolean} isMain - Whether it's the main parts or not * @param {boolean} isOutline - Whether a path is an outline or not * @private diff --git a/src/mapml/features/featureGroup.js b/src/mapml/features/geometry.js similarity index 94% rename from src/mapml/features/featureGroup.js rename to src/mapml/features/geometry.js index 906cc86ae..b35a1d479 100644 --- a/src/mapml/features/featureGroup.js +++ b/src/mapml/features/geometry.js @@ -1,13 +1,13 @@ -export var FeatureGroup = L.FeatureGroup.extend({ +export var Geometry = L.FeatureGroup.extend({ /** * Initialize the feature group - * @param {M.Feature[]} layers + * @param {M.Path[]} layers * @param {Object} options */ initialize: function (layers, options) { if (options.wrappers && options.wrappers.length > 0) options = Object.assign( - M.Feature.prototype._convertWrappers(options.wrappers), + M.Path.prototype._convertWrappers(options.wrappers), options ); @@ -23,7 +23,7 @@ export var FeatureGroup = L.FeatureGroup.extend({ if (layers.length === 1 && firstLayer.options.link) this.options.link = firstLayer.options.link; if (this.options.link) { - M.Feature.prototype.attachLinkHandler.call( + M.Path.prototype.attachLinkHandler.call( this, this.options.group, this.options.link, @@ -190,7 +190,7 @@ export var FeatureGroup = L.FeatureGroup.extend({ }, /** - * Add a M.Feature to the M.FeatureGroup + * Add a M.Path to the M.Geometry * @param layer */ addLayer: function (layer) { @@ -253,11 +253,11 @@ export var FeatureGroup = L.FeatureGroup.extend({ }); /** - * Returns new M.FeatureGroup - * @param {M.Feature[]} layers - Layers belonging to feature group + * Returns new M.Geometry + * @param {M.Path[]} layers - Layers belonging to feature group * @param {Object} options - Options for the feature group - * @returns {M.FeatureGroup} + * @returns {M.Geometry} */ -export var featureGroup = function (layers, options) { - return new FeatureGroup(layers, options); +export var geometry = function (layers, options) { + return new Geometry(layers, options); }; diff --git a/src/mapml/features/feature.js b/src/mapml/features/path.js similarity index 92% rename from src/mapml/features/feature.js rename to src/mapml/features/path.js index cb501139e..00de8204f 100644 --- a/src/mapml/features/feature.js +++ b/src/mapml/features/path.js @@ -1,5 +1,5 @@ /** - * M.Feature is a extension of L.Path that understands mapml feature markup + * M.Path is a extension of L.Path that understands mapml feature markup * It converts the markup to the following structure (abstract enough to encompass all feature types) for example: * this._outlinePath = HTMLElement; * this._parts = [ @@ -18,9 +18,9 @@ * ... * ]; */ -export var Feature = L.Path.extend({ +export var Path = L.Path.extend({ /** - * Initializes the M.Feature + * Initializes the M.Path * @param {HTMLElement} markup - The markup representation of the feature * @param {Object} options - The options of the feature */ @@ -75,11 +75,16 @@ export var Feature = L.Path.extend({ nextLayer = this.options._leafletLayer._layerEl.nextElementSibling; while (nextLayer && onTop) { if (nextLayer.tagName && nextLayer.tagName.toUpperCase() === 'LAYER-') - onTop = !(nextLayer.checked && nextLayer._layer.queryable); + onTop = !( + nextLayer.checked && + nextLayer._layer && + nextLayer._layer.queryable + ); nextLayer = nextLayer.nextElementSibling; } if (onTop && dragStart) { - //M._handleLink gets called twice, once in the target phase on the path element, then in the bubble phase on the g element + //M._handleLink gets called twice, once in the target phase on the path + //element, then in the bubble phase on the g element //Using stopPropagation leaves the mouse in the mousedown state if (e.eventPhase === Event.BUBBLING_PHASE) return; let dist = Math.sqrt( @@ -142,17 +147,29 @@ export var Feature = L.Path.extend({ }, this ); - L.DomEvent.on( - leafletLayer._map.getContainer(), - 'mouseout mouseenter click', - (e) => { - //adds a lot of event handlers - if (!container.parentElement) return; - hovered = false; - this._map.getContainer().removeChild(container); - }, - this - ); + leafletLayer.on('add', addMouseHandler, leafletLayer); + function handleMouse(e) { + //adds a lot of event handlers + if (!container.parentElement) return; + hovered = false; + this._map.getContainer().removeChild(container); + } + function addMouseHandler() { + L.DomEvent.on( + this._map.getContainer(), + 'mouseout mouseenter click', + handleMouse, + this + ); + } + leafletLayer.on('remove', removeMouseHandler, leafletLayer); + function removeMouseHandler() { + L.DomEvent.off(this._map.getContainer(), { + mouseout: handleMouse, + mouseenter: handleMouse, + click: handleMouse + }); + } }, /** @@ -457,8 +474,8 @@ export var Feature = L.Path.extend({ * * @param {HTMLElement} markup - The markup of the feature * @param {Object} options - Options of the feature - * @returns {M.Feature} + * @returns {M.Path} */ -export var feature = function (markup, options) { - return new Feature(markup, options); +export var path = function (markup, options) { + return new Path(markup, options); }; diff --git a/src/mapml/handlers/AnnounceMovement.js b/src/mapml/handlers/AnnounceMovement.js index f0beef557..960efdd3a 100644 --- a/src/mapml/handlers/AnnounceMovement.js +++ b/src/mapml/handlers/AnnounceMovement.js @@ -5,7 +5,10 @@ export var AnnounceMovement = L.Handler.extend({ layerremove: this.totalBounds }); - this._map.options.mapEl.addEventListener('moveend', this.announceBounds); + this._map.options.mapEl.addEventListener( + 'map-moveend', + this.announceBounds + ); this._map.dragging._draggable.addEventListener('dragstart', this.dragged); this._map.options.mapEl.addEventListener( 'mapfocused', @@ -18,7 +21,10 @@ export var AnnounceMovement = L.Handler.extend({ layerremove: this.totalBounds }); - this._map.options.mapEl.removeEventListener('moveend', this.announceBounds); + this._map.options.mapEl.removeEventListener( + 'map-moveend', + this.announceBounds + ); this._map.dragging._draggable.removeEventListener( 'dragstart', this.dragged diff --git a/src/mapml/handlers/QueryHandler.js b/src/mapml/handlers/QueryHandler.js index b0a494503..1301084e2 100644 --- a/src/mapml/handlers/QueryHandler.js +++ b/src/mapml/handlers/QueryHandler.js @@ -16,7 +16,7 @@ export var QueryHandler = L.Handler.extend({ // work backwards in document order (top down) for (var l = layers.length - 1; l >= 0; l--) { var mapmlLayer = layers[l]._layer; - if (layers[l].checked && mapmlLayer.queryable) { + if (layers[l].checked && mapmlLayer && mapmlLayer.queryable) { return mapmlLayer; } } @@ -50,7 +50,7 @@ export var QueryHandler = L.Handler.extend({ _query(e, layer) { var zoom = e.target.getZoom(), map = this._map, - crs = layer._extent.crs, // the crs for each extent would be the same + crs = layer._properties.crs, // the crs for each extent would be the same tileSize = map.options.crs.options.crs.tile.bounds.max.x, container = layer._container, popupOptions = { @@ -82,9 +82,13 @@ export var QueryHandler = L.Handler.extend({ ); let templates = layer.getQueryTemplates(pcrsClick); - var fetchFeatures = function (template, obj, lastOne) { + let fetches = []; + + var fetchFeatures = function (template, obj) { const parser = new DOMParser(); - fetch(L.Util.template(template.template, obj), { redirect: 'follow' }) + return fetch(L.Util.template(template.template, obj), { + redirect: 'follow' + }) .then((response) => { if (response.status >= 200 && response.status < 300) { return response.text().then((text) => { @@ -98,25 +102,23 @@ export var QueryHandler = L.Handler.extend({ } }) .then((response) => { - if (!layer._mapmlFeatures) layer._mapmlFeatures = []; + let features = []; if (response.contenttype.startsWith('text/mapml')) { // the mapmldoc could have elements that are important, perhaps // also, the mapmldoc can have many features let mapmldoc = parser.parseFromString( - response.text, - 'application/xml' - ), - features = Array.prototype.slice.call( - mapmldoc.querySelectorAll('map-feature') - ); + response.text, + 'application/xml' + ); + features = Array.prototype.slice.call( + mapmldoc.querySelectorAll('map-feature') + ); // elements layer.metas = Array.prototype.slice.call( mapmldoc.querySelectorAll( 'map-meta[name=cs], map-meta[name=zoom], map-meta[name=projection]' ) ); - if (features.length) - layer._mapmlFeatures = layer._mapmlFeatures.concat(features); } else { // synthesize a single feature from text or html content let geom = @@ -135,15 +137,9 @@ export var QueryHandler = L.Handler.extend({ 'text/html' ) .querySelector('map-feature'); - layer._mapmlFeatures.push(feature); - } - if (lastOne) { - // create connection between queried and its parent - for (let feature of layer._mapmlFeatures) { - feature._extentEl = template._extentEl; - } - displayFeaturesPopup(layer._mapmlFeatures, e.latlng); + features.push(feature); } + return { features: features, template: template }; }) .catch((err) => { console.log('Looks like there was a problem. Status: ' + err.message); @@ -247,10 +243,26 @@ export var QueryHandler = L.Handler.extend({ } if (template.extentBounds.contains(pcrsClick)) { - let lastOne = i === templates.length - 1 ? true : false; - fetchFeatures(template, obj, lastOne); + fetches.push(fetchFeatures(template, obj)); } } + Promise.allSettled(fetches).then((results) => { + layer._mapmlFeatures = []; + // f is an array of {features[], template} + + for (let f of results) { + if (f.status === 'fulfilled') { + // create connection between queried and its parent + for (let feature of f.value.features) { + feature._extentEl = f.value.template._extentEl; + } + layer._mapmlFeatures = layer._mapmlFeatures.concat(f.value.features); + } + } + if (layer._mapmlFeatures.length > 0) + displayFeaturesPopup(layer._mapmlFeatures, e.latlng); + }); + function displayFeaturesPopup(features, loc) { if (features.length === 0) return; let f = M.featureLayer(features, { diff --git a/src/mapml/index.js b/src/mapml/index.js index 3fd0fef7a..cd05e6b5a 100644 --- a/src/mapml/index.js +++ b/src/mapml/index.js @@ -69,9 +69,9 @@ import { ScaleBar, scaleBar } from './control/ScaleBar'; import { FullscreenButton, fullscreenButton } from './control/FullscreenButton'; import { geolocationButton } from './control/GeolocationButton'; import { Crosshair, crosshair } from './layers/Crosshair'; -import { Feature, feature } from './features/feature'; +import { Path, path } from './features/path'; import { FeatureRenderer, featureRenderer } from './features/featureRenderer'; -import { FeatureGroup, featureGroup } from './features/featureGroup'; +import { Geometry, geometry } from './features/geometry'; import { AnnounceMovement } from './handlers/AnnounceMovement'; import { FeatureIndex } from './handlers/FeatureIndex'; import { Options } from './options'; @@ -827,6 +827,9 @@ import { M._pcrsToGcrs = Util._pcrsToGcrs; M.mapml2geojson = Util.mapml2geojson; M.getMaxZoom = Util.getMaxZoom; + M.getBounds = Util.getBounds; + M.getZoomBounds = Util.getZoomBounds; + M.getNativeVariables = Util.getNativeVariables; M.QueryHandler = QueryHandler; M.ContextMenu = ContextMenu; @@ -889,12 +892,12 @@ import { M.FeatureIndexOverlay = FeatureIndexOverlay; M.featureIndexOverlay = featureIndexOverlay; - M.Feature = Feature; - M.feature = feature; + M.Path = Path; + M.path = path; M.FeatureRenderer = FeatureRenderer; M.featureRenderer = featureRenderer; - M.FeatureGroup = FeatureGroup; - M.featureGroup = featureGroup; + M.Geometry = Geometry; + M.geometry = geometry; })(window, document); diff --git a/src/mapml/layers/FeatureIndexOverlay.js b/src/mapml/layers/FeatureIndexOverlay.js index 813f67a5a..677cd39e7 100644 --- a/src/mapml/layers/FeatureIndexOverlay.js +++ b/src/mapml/layers/FeatureIndexOverlay.js @@ -24,11 +24,7 @@ export var FeatureIndexOverlay = L.Layer.extend({ ); this._body.index = 0; this._output.initialFocus = false; - map.on( - 'layerchange layeradd layerremove overlayremove', - this._toggleEvents, - this - ); + map.on('focus blur popupclose', this._addOrRemoveFeatureIndex, this); map.on('moveend focus templatedfeatureslayeradd', this._checkOverlap, this); map.on('keydown', this._onKeyDown, this); this._addOrRemoveFeatureIndex(); @@ -77,6 +73,8 @@ export var FeatureIndexOverlay = L.Layer.extend({ let index = 1; let keys = Object.keys(features); let body = this._body; + let noFeaturesMessage = document.createElement('span'); + noFeaturesMessage.innerHTML = M.options.locale.fIndexNoFeatures; body.innerHTML = ''; body.index = 0; @@ -123,6 +121,9 @@ export var FeatureIndexOverlay = L.Layer.extend({ } }); this._addToggleKeys(); + if (index === 1) { + body.appendChild(noFeaturesMessage); + } }, _updateOutput: function (label, index, key) { @@ -189,16 +190,7 @@ export var FeatureIndexOverlay = L.Layer.extend({ } }, - _toggleEvents: function () { - this._map.on( - 'viewreset move moveend focus blur popupclose', - this._addOrRemoveFeatureIndex, - this - ); - }, - _addOrRemoveFeatureIndex: function (e) { - let features = this._body.allFeatures ? this._body.allFeatures.length : 0; //Toggle aria-hidden attribute so screen reader rereads the feature index on focus if (!this._output.initialFocus) { this._output.setAttribute('aria-hidden', 'true'); @@ -214,11 +206,15 @@ export var FeatureIndexOverlay = L.Layer.extend({ this._output.popupClosed = true; } else if (e && e.type === 'focus') { this._container.removeAttribute('hidden'); - if (features !== 0) - this._output.classList.remove('mapml-screen-reader-output'); - } else if (e && e.originalEvent && e.originalEvent.type === 'pointermove') { - this._container.setAttribute('hidden', ''); - this._output.classList.add('mapml-screen-reader-output'); + this._output.classList.remove('mapml-screen-reader-output'); + // this is a very subtle branch. The event that gets handled below is a blur + // event, which happens to have the e.target._popup property + // when there will be a popup. Because blur gets handled here, it doesn't + // get handled in the next else if block, which would hide both the reticle + // and the index menu, and then recursively call this method with no event + // argument, which manipulates the aria-hidden attribute on the output + // in order to have the screenreader read its contents when the focus returns + // to (what exactly???). } else if (e && e.target._popup) { this._container.setAttribute('hidden', ''); } else if (e && e.type === 'blur') { @@ -226,14 +222,8 @@ export var FeatureIndexOverlay = L.Layer.extend({ this._output.classList.add('mapml-screen-reader-output'); this._output.initialFocus = false; this._addOrRemoveFeatureIndex(); - } else if (this._map.isFocused && e) { - this._container.removeAttribute('hidden'); - if (features !== 0) { - this._output.classList.remove('mapml-screen-reader-output'); - } else { - this._output.classList.add('mapml-screen-reader-output'); - } } else { + // this is the default block, called when no event is passed (recursive call) this._container.setAttribute('hidden', ''); this._output.classList.add('mapml-screen-reader-output'); } diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index 0a0956683..f37d90942 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -3,6 +3,8 @@ import { FALLBACK_CS, FALLBACK_PROJECTION } from '../utils/Constants'; export var FeatureLayer = L.FeatureGroup.extend({ /* * M.MapML turns any MapML feature data into a Leaflet layer. Based on L.GeoJSON. + * + * Used by MapMLLayer to create _mapmlvectors property, used to render features */ initialize: function (mapml, options) { /* @@ -11,6 +13,11 @@ export var FeatureLayer = L.FeatureGroup.extend({ 2. for static templated feature: null 3. for non-templated feature: layer- (with no src) or mapml file (with src) */ + // options.extent: when you use a FeatureLayer, you can either get it to calculate the + // .layerBounds dynamically (the default), based on adds/removes of features from the layer/ + // or you can construct it with a bounds (via options.extent), + // which will then remain static for the lifetime of the layer + L.setOptions(this, options); if (this.options.static) { this._container = L.DomUtil.create( @@ -26,40 +33,36 @@ export var FeatureLayer = L.FeatureGroup.extend({ ); L.setOptions(this.options.renderer, { pane: this._container }); } - this._layers = {}; if (this.options.query) { this._mapmlFeatures = mapml.features ? mapml.features : mapml; this.isVisible = true; - let native = this._getNativeVariables(mapml); + let native = M.getNativeVariables(mapml); this.options.nativeZoom = native.zoom; this.options.nativeCS = native.cs; - } - if (mapml && !this.options.query) { - let native = this._getNativeVariables(mapml); - //needed to check if the feature is static or not, since this method is used by templated also - if ( - !mapml.querySelector('map-extent') && - mapml.querySelector('map-feature') && - this.options.static - ) { - this._features = {}; - this._staticFeature = true; - this.isVisible = true; //placeholder for when this actually gets updated in the future - this.zoomBounds = this._getZoomBounds(mapml, native.zoom); - this.layerBounds = this._getLayerBounds(mapml); - L.extend(this.options, this.zoomBounds); - } - this.addData(mapml, native.cs, native.zoom); - if (this._staticFeature) { - this._resetFeatures(); - this.options._leafletLayer._map._addZoomLimit(this); + } else { + if (mapml) { + let native = M.getNativeVariables(mapml); + this.addData(mapml, native.cs, native.zoom); + } else if (!mapml) { + this.isVisible = false; + // use this.options._leafletLayer to distinguish the featureLayer constructed for initialization and for templated features / tiles + if (this.options._leafletLayer) { + // this._staticFeature should be set to true to make sure the _getEvents works properly + this._features = {}; + this._staticFeature = true; + } } } }, onAdd: function (map) { + this._map = map; L.FeatureGroup.prototype.onAdd.call(this, map); + if (this._staticFeature) { + this._resetFeatures(); + this.options._leafletLayer._map._addZoomLimit(this); + } if (this._mapmlFeatures) map.on('featurepagination', this.showPaginationFeature, this); }, @@ -74,6 +77,11 @@ export var FeatureLayer = L.FeatureGroup.extend({ this._map.featureIndex.cleanIndex(); }, + removeLayer: function (featureGroupLayer) { + L.FeatureGroup.prototype.removeLayer.call(this, featureGroupLayer); + delete this._layers[featureGroupLayer._leaflet_id]; + }, + getEvents: function () { if (this._staticFeature) { return { @@ -128,59 +136,12 @@ export var FeatureLayer = L.FeatureGroup.extend({ } }, - // _getNativeVariables: returns an object with the native zoom and CS, - // based on the map-metas that are available within - // the layer or the fallback default values. - // _getNativeVariables: mapml-||layer-||null||[map-feature,...] -> {zoom: _, val: _} - // mapml can be a mapml- element, layer- element, null, or an array of map-features - _getNativeVariables: function (mapml) { - let nativeZoom, nativeCS; - // when mapml is an array of features provided by the query - if ( - mapml.length && - mapml[0].parentElement.parentElement && - mapml[0].parentElement.parentElement.tagName === 'mapml-' - ) { - let mapmlEl = mapml[0].parentElement.parentElement; - nativeZoom = - (mapmlEl.querySelector && - mapmlEl.querySelector('map-meta[name=zoom]') && - +M._metaContentToObject( - mapmlEl.querySelector('map-meta[name=zoom]').getAttribute('content') - ).value) || - 0; - nativeCS = - (mapmlEl.querySelector && - mapmlEl.querySelector('map-meta[name=cs]') && - M._metaContentToObject( - mapmlEl.querySelector('map-meta[name=cs]').getAttribute('content') - ).content) || - 'GCRS'; - } else { - // when mapml is null or a layer-/mapml- element - nativeZoom = - (mapml.querySelector && - mapml.querySelector('map-meta[name=zoom]') && - +M._metaContentToObject( - mapml.querySelector('map-meta[name=zoom]').getAttribute('content') - ).value) || - 0; - nativeCS = - (mapml.querySelector && - mapml.querySelector('map-meta[name=cs]') && - M._metaContentToObject( - mapml.querySelector('map-meta[name=cs]').getAttribute('content') - ).content) || - 'GCRS'; - } - return { zoom: nativeZoom, cs: nativeCS }; - }, - _handleMoveEnd: function () { let mapZoom = this._map.getZoom(), - withinZoom = - mapZoom <= this.zoomBounds.maxZoom && - mapZoom >= this.zoomBounds.minZoom; + withinZoom = this.zoomBounds + ? mapZoom <= this.zoomBounds.maxZoom && + mapZoom >= this.zoomBounds.minZoom + : false; this.isVisible = withinZoom && this._layers && @@ -196,87 +157,48 @@ export var FeatureLayer = L.FeatureGroup.extend({ }, _handleZoomEnd: function (e) { - let mapZoom = this._map.getZoom(); - if ( - mapZoom > this.zoomBounds.maxZoom || - mapZoom < this.zoomBounds.minZoom - ) { - this.clearLayers(); - return; - } - this._resetFeatures(); - }, - - //sets default if any are missing, better to only replace ones that are missing - _getLayerBounds: function (container) { - if (!container) return null; - let cs = FALLBACK_CS, - projection = - (container.querySelector('map-meta[name=projection]') && - M._metaContentToObject( - container - .querySelector('map-meta[name=projection]') - .getAttribute('content') - ).content.toUpperCase()) || - FALLBACK_PROJECTION; - try { - let meta = - container.querySelector('map-meta[name=extent]') && - M._metaContentToObject( - container - .querySelector('map-meta[name=extent]') - .getAttribute('content') - ); - - let zoom = meta.zoom || 0; - - let metaKeys = Object.keys(meta); - for (let i = 0; i < metaKeys.length; i++) { - if (!metaKeys[i].includes('zoom')) { - cs = M.axisToCS(metaKeys[i].split('-')[2]); - break; - } - } - let axes = M.csToAxes(cs); - return M.boundsToPCRSBounds( - L.bounds( - L.point(+meta[`top-left-${axes[0]}`], +meta[`top-left-${axes[1]}`]), - L.point( - +meta[`bottom-right-${axes[0]}`], - +meta[`bottom-right-${axes[1]}`] - ) - ), - zoom, - projection, - cs - ); - } catch (error) { - //if error then by default set the layer to osm and bounds to the entire map view - return M.boundsToPCRSBounds( - M[projection].options.crs.tilematrix.bounds(0), - 0, - projection, - cs - ); + // handle zoom end gets called twice for every zoom, this condition makes it go through once only. + if (this.zoomBounds) { + this._resetFeatures(); } }, + // remove or add features based on the min max attribute of the features, + // and add placeholders to maintain position _resetFeatures: function () { - this.clearLayers(); // since features are removed and re-added by zoom level, need to clean the feature index before re-adding if (this._map) this._map.featureIndex.cleanIndex(); let map = this._map || this.options._leafletLayer._map; if (this._features) { for (let zoom in this._features) { for (let k = 0; k < this._features[zoom].length; k++) { - let feature = this._features[zoom][k], - checkRender = feature._checkRender( + let featureGroupLayer = this._features[zoom][k], + checkRender = featureGroupLayer._checkRender( map.getZoom(), this.zoomBounds.minZoom, this.zoomBounds.maxZoom ); - if (checkRender) { - this.addLayer(feature); + if (!checkRender) { + let placeholder = document.createElement('span'); + placeholder.id = featureGroupLayer._leaflet_id; + featureGroupLayer.defaultOptions.group.insertAdjacentElement( + 'beforebegin', + placeholder + ); + // removing the rendering without removing the feature from the feature list + this.removeLayer(featureGroupLayer); + } else if ( + // checking for _map so we do not enter this code block during the connectedCallBack of the map-feature + !map.hasLayer(featureGroupLayer) && + !featureGroupLayer._map + ) { + this.addLayer(featureGroupLayer); + // update the layerbounds + let placeholder = + featureGroupLayer.defaultOptions.group.parentNode.querySelector( + `span[id="${featureGroupLayer._leaflet_id}"]` + ); + placeholder.replaceWith(featureGroupLayer.defaultOptions.group); } } } @@ -297,46 +219,10 @@ export var FeatureLayer = L.FeatureGroup.extend({ } }, - _getZoomBounds: function (container, nativeZoom) { - if (!container) return null; - let nMin = 100, - nMax = 0, - features = container.querySelectorAll('map-feature'), - meta, - projection; - for (let i = 0; i < features.length; i++) { - let lZoom = +features[i].getAttribute('zoom'); - if (!features[i].getAttribute('zoom')) lZoom = nativeZoom; - nMax = Math.max(nMax, lZoom); - nMin = Math.min(nMin, lZoom); - } - try { - projection = M._metaContentToObject( - container - .querySelector('map-meta[name=projection]') - .getAttribute('content') - ).content; - meta = M._metaContentToObject( - container.querySelector('map-meta[name=zoom]').getAttribute('content') - ); - } catch (error) { - return { - minZoom: 0, - maxZoom: - M[projection || FALLBACK_PROJECTION].options.resolutions.length - 1, - minNativeZoom: nMin, - maxNativeZoom: nMax - }; - } - return { - minZoom: +meta.min, - maxZoom: +meta.max, - minNativeZoom: nMin, - maxNativeZoom: nMax - }; - }, - addData: function (mapml, nativeCS, nativeZoom) { + if (mapml) { + this.isVisible = true; + } var features = mapml.nodeType === Node.DOCUMENT_NODE || mapml.nodeName === 'LAYER-' ? mapml.getElementsByTagName('map-feature') @@ -481,7 +367,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ 'map-polygon, map-linestring, map-multilinestring, map-point, map-multipoint' )) { group.push( - M.feature( + M.path( geo, Object.assign(copyOptions, { nativeCS: cs, @@ -512,7 +398,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ groupOptions.wrappers = this._getGeometryParents( collections.parentElement ); - return M.featureGroup(group, groupOptions); + return M.geometry(group, groupOptions); } }, diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 48cfaa398..015fa08f0 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1,3 +1,5 @@ +/* global M */ + import { FALLBACK_PROJECTION, BLANK_TT_TREF } from '../utils/Constants'; export var MapMLLayer = L.Layer.extend({ @@ -11,22 +13,18 @@ export var MapMLLayer = L.Layer.extend({ opacity: '1.0' }, // initialize is executed before the layer is added to a map - initialize: function (href, content, options) { + initialize: function (href, layerEl, mapml, options) { // in the custom element, the attribute is actually 'src' // the _href version is the URL received from layer-@src - var mapml; if (href) { this._href = href; } - if (content) { - this._layerEl = content; - mapml = content.querySelector('map-feature,map-tile,map-extent') - ? true - : false; - if (!href && mapml) { - this._content = content; - } - } + let local; + this._layerEl = layerEl; + local = layerEl.querySelector('map-feature,map-tile,map-extent') + ? true + : false; + this._content = local ? layerEl : mapml; L.setOptions(this, options); this._container = L.DomUtil.create('div', 'leaflet-layer'); this.changeOpacity(this.options.opacity); @@ -48,9 +46,7 @@ export var MapMLLayer = L.Layer.extend({ // hit the service to determine what its extent might be // OR use the extent of the content provided - if (!mapml && content && content.hasAttribute('label')) - this._title = content.getAttribute('label'); - this._initialize(mapml ? content : null); + this._initialize(local ? layerEl : mapml); // a default extent can't be correctly set without the map to provide // its bounds , projection, zoom range etc, so if that stuff's not @@ -63,10 +59,6 @@ export var MapMLLayer = L.Layer.extend({ // above. Not going to change this, but failing to understand ATM. // may revisit some time. this.validProjection = true; - - // _mapmlLayerItem is set to the root element representing this layer - // in the layer control, iff the layer is not 'hidden' - this._mapmlLayerItem = {}; }, setZIndex: function (zIndex) { this.options.zIndex = zIndex; @@ -88,15 +80,15 @@ export var MapMLLayer = L.Layer.extend({ }, // remove all the extents before removing the layer from the map _removeExtents: function (map) { - if (this._extent._mapExtents) { - for (let i = 0; i < this._extent._mapExtents.length; i++) { - if (this._extent._mapExtents[i].templatedLayer) { - map.removeLayer(this._extent._mapExtents[i].templatedLayer); + if (this._properties._mapExtents) { + for (let i = 0; i < this._properties._mapExtents.length; i++) { + if (this._properties._mapExtents[i].templatedLayer) { + map.removeLayer(this._properties._mapExtents[i].templatedLayer); } } } - if (this._extent._queries) { - delete this._extent._queries; + if (this._properties._queries) { + delete this._properties._queries; } }, _changeOpacity: function (e) { @@ -106,6 +98,7 @@ export var MapMLLayer = L.Layer.extend({ }, changeOpacity: function (opacity) { this._container.style.opacity = opacity; + this._layerEl._opacity = opacity; if (this.opacityEl) this.opacityEl.value = opacity; }, _changeExtentOpacity: function (e) { @@ -136,11 +129,14 @@ export var MapMLLayer = L.Layer.extend({ this._setLayerElExtent(); } }, + titleIsReadOnly() { + return !!this._titleIsReadOnly; + }, setName(newName) { // a layer's accessible name is set by the , if present // if it's not available the attribute // can be used - if (!this._titleIsReadOnly) { + if (!this.titleIsReadOnly()) { this._title = newName; this._mapmlLayerItem.querySelector('.mapml-layer-item-name').innerHTML = newName; @@ -151,75 +147,12 @@ export var MapMLLayer = L.Layer.extend({ }, onAdd: function (map) { - if (this._extent && !this._validProjection(map)) { + if (this._properties && !this._validProjection(map)) { this.validProjection = false; return; } this._map = map; - if (this._content) { - if (!this._mapmlvectors) { - this._mapmlvectors = M.featureLayer(this._content, { - // pass the vector layer a renderer of its own, otherwise leaflet - // puts everything into the overlayPane - renderer: M.featureRenderer(), - // pass the vector layer the container for the parent into which - // it will append its own container for rendering into - pane: this._container, - opacity: this.options.opacity, - projection: map.options.projection, - // each owned child layer gets a reference to the root layer - _leafletLayer: this, - static: true, - onEachFeature: function (properties, geometry) { - // need to parse as HTML to preserve semantics and styles - if (properties) { - var c = document.createElement('div'); - c.classList.add('mapml-popup-content'); - c.insertAdjacentHTML('afterbegin', properties.innerHTML); - geometry.bindPopup(c, { autoClose: false, minWidth: 165 }); - } - } - }); - } - this._setLayerElExtent(); - map.addLayer(this._mapmlvectors); - } else { - this.once( - 'extentload', - function () { - if (!this._validProjection(map)) { - this.validProjection = false; - return; - } - if (!this._mapmlvectors) { - this._mapmlvectors = M.featureLayer(this._content, { - // pass the vector layer a renderer of its own, otherwise leaflet - // puts everything into the overlayPane - renderer: M.featureRenderer(), - // pass the vector layer the container for the parent into which - // it will append its own container for rendering into - pane: this._container, - opacity: this.options.opacity, - projection: map.options.projection, - // each owned child layer gets a reference to the root layer - _leafletLayer: this, - static: true, - onEachFeature: function (properties, geometry) { - // need to parse as HTML to preserve semantics and styles - if (properties) { - var c = document.createElement('div'); - c.classList.add('mapml-popup-content'); - c.insertAdjacentHTML('afterbegin', properties.innerHTML); - geometry.bindPopup(c, { autoClose: false, minWidth: 165 }); - } - } - }).addTo(map); - } - this._setLayerElExtent(); - }, - this - ); - } + if (this._mapmlvectors) map.addLayer(this._mapmlvectors); if (!this._imageLayer) { this._imageLayer = L.layerGroup(); @@ -229,44 +162,20 @@ export var MapMLLayer = L.Layer.extend({ // content will be maintained //only add the layer if there are tiles to be rendered - if ( - (!this._staticTileLayer || this._staticTileLayer._container === null) && - this._mapmlTileContainer.getElementsByTagName('map-tiles').length > 0 - ) { - this._staticTileLayer = M.staticTileLayer({ - pane: this._container, - _leafletLayer: this, - className: 'mapml-static-tile-layer', - tileContainer: this._mapmlTileContainer, - maxZoomBound: map.options.crs.options.resolutions.length - 1, - tileSize: map.options.crs.options.crs.tile.bounds.max.x - }); + if (this._staticTileLayer) { map.addLayer(this._staticTileLayer); - this._setLayerElExtent(); } const createAndAdd = createAndAddTemplatedLayers.bind(this); // if the extent has been initialized and received, update the map, if ( - this._extent && - this._extent._mapExtents && - this._extent._mapExtents[0]._templateVars + this._properties && + this._properties._mapExtents && + this._properties._mapExtents[0]._templateVars ) { createAndAdd(); - } else { - // wait for extent to be loaded - this.once( - 'extentload', - function () { - if (!this._validProjection(map)) { - this.validProjection = false; - return; - } - createAndAdd(); - }, - this - ); } + this._setLayerElExtent(); this.setZIndex(this.options.zIndex); this.getPane().appendChild(this._container); @@ -276,53 +185,56 @@ export var MapMLLayer = L.Layer.extend({ map.on('popupopen', this._attachSkipButtons, this); function createAndAddTemplatedLayers() { - if (this._extent && this._extent._mapExtents) { - for (let i = 0; i < this._extent._mapExtents.length; i++) { + if (this._properties && this._properties._mapExtents) { + for (let i = 0; i < this._properties._mapExtents.length; i++) { if ( - this._extent._mapExtents[i]._templateVars && - this._extent._mapExtents[i].checked + this._properties._mapExtents[i]._templateVars && + this._properties._mapExtents[i].checked ) { - if (!this._extent._mapExtents[i].extentZIndex) - this._extent._mapExtents[i].extentZIndex = i; + if (!this._properties._mapExtents[i].extentZIndex) + this._properties._mapExtents[i].extentZIndex = i; this._templatedLayer = M.templatedLayer( - this._extent._mapExtents[i]._templateVars, + this._properties._mapExtents[i]._templateVars, { pane: this._container, - opacity: this._extent._mapExtents[i]._templateVars.opacity, + opacity: this._properties._mapExtents[i]._templateVars.opacity, _leafletLayer: this, - crs: this._extent.crs, - extentZIndex: this._extent._mapExtents[i].extentZIndex, + crs: this._properties.crs, + extentZIndex: this._properties._mapExtents[i].extentZIndex, // when a migrates from a remote mapml file and attaches to the shadow of - // this._extent._mapExtents[i] refers to the in remote mapml + // this._properties._mapExtents[i] refers to the in remote mapml extentEl: - this._extent._mapExtents[i]._DOMnode || - this._extent._mapExtents[i] + this._properties._mapExtents[i]._DOMnode || + this._properties._mapExtents[i] } ).addTo(map); - this._extent._mapExtents[i].templatedLayer = this._templatedLayer; + this._properties._mapExtents[i].templatedLayer = + this._templatedLayer; if (this._templatedLayer._queries) { - if (!this._extent._queries) this._extent._queries = []; - this._extent._queries = this._extent._queries.concat( + if (!this._properties._queries) this._properties._queries = []; + this._properties._queries = this._properties._queries.concat( this._templatedLayer._queries ); } } - if (this._extent._mapExtents[i].hasAttribute('opacity')) { - let opacity = this._extent._mapExtents[i].getAttribute('opacity'); - this._extent._mapExtents[i].templatedLayer.changeOpacity(opacity); + if (this._properties._mapExtents[i].hasAttribute('opacity')) { + let opacity = + this._properties._mapExtents[i].getAttribute('opacity'); + this._properties._mapExtents[i].templatedLayer.changeOpacity( + opacity + ); } } - this._setLayerElExtent(); } } }, _validProjection: function (map) { let noLayer = false; - if (this._extent && this._extent._mapExtents) { - for (let i = 0; i < this._extent._mapExtents.length; i++) { - if (this._extent._mapExtents[i]._templateVars) { - for (let template of this._extent._mapExtents[i]._templateVars) + if (this._properties && this._properties._mapExtents) { + for (let i = 0; i < this._properties._mapExtents.length; i++) { + if (this._properties._mapExtents[i]._templateVars) { + for (let template of this._properties._mapExtents[i]._templateVars) if ( !template.projectionMatch && template.projection !== map.options.projection @@ -333,9 +245,7 @@ export var MapMLLayer = L.Layer.extend({ } } } - return !( - noLayer || this.getProjection() !== map.options.projection.toUpperCase() - ); + return !(noLayer || this.getProjection() !== map.options.projection); }, //sets the elements .bounds property @@ -360,71 +270,73 @@ export var MapMLLayer = L.Layer.extend({ layerTypes.forEach((type) => { if (this[type]) { if (type === '_templatedLayer') { - for (let i = 0; i < this._extent._mapExtents.length; i++) { + for (let i = 0; i < this._properties._mapExtents.length; i++) { for ( let j = 0; - j < this._extent._mapExtents[i]._templateVars.length; + j < this._properties._mapExtents[i]._templateVars.length; j++ ) { let inputData = M._extractInputBounds( - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] ); - this._extent._mapExtents[i]._templateVars[j].tempExtentBounds = - inputData.bounds; - this._extent._mapExtents[i]._templateVars[j].extentZoomBounds = - inputData.zoomBounds; + this._properties._mapExtents[i]._templateVars[ + j + ].tempExtentBounds = inputData.bounds; + this._properties._mapExtents[i]._templateVars[ + j + ].extentZoomBounds = inputData.zoomBounds; } } - for (let i = 0; i < this._extent._mapExtents.length; i++) { - if (this._extent._mapExtents[i].checked) { + for (let i = 0; i < this._properties._mapExtents.length; i++) { + if (this._properties._mapExtents[i].checked) { for ( let j = 0; - j < this._extent._mapExtents[i]._templateVars.length; + j < this._properties._mapExtents[i]._templateVars.length; j++ ) { if (!bounds) { bounds = - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .tempExtentBounds; zoomMax = - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.maxZoom; zoomMin = - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.minZoom; maxNativeZoom = - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.maxNativeZoom; minNativeZoom = - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.minNativeZoom; } else { bounds.extend( - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .tempExtentBounds.min ); bounds.extend( - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .tempExtentBounds.max ); zoomMax = Math.max( zoomMax, - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.maxZoom ); zoomMin = Math.min( zoomMin, - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.minZoom ); maxNativeZoom = Math.max( maxNativeZoom, - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.maxNativeZoom ); minNativeZoom = Math.min( minNativeZoom, - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.minNativeZoom ); } @@ -435,14 +347,39 @@ export var MapMLLayer = L.Layer.extend({ zoomBounds.maxZoom = zoomMax; zoomBounds.minNativeZoom = minNativeZoom; zoomBounds.maxNativeZoom = maxNativeZoom; - this._extent.zoomBounds = zoomBounds; - this._extent.layerBounds = bounds; + this._properties.zoomBounds = zoomBounds; + this._properties.layerBounds = bounds; // assign each template the layer and zoom bounds - for (let i = 0; i < this._extent._mapExtents.length; i++) { - this._extent._mapExtents[i].templatedLayer.layerBounds = bounds; - this._extent._mapExtents[i].templatedLayer.zoomBounds = zoomBounds; + for (let i = 0; i < this._properties._mapExtents.length; i++) { + this._properties._mapExtents[i].templatedLayer.layerBounds = bounds; + this._properties._mapExtents[i].templatedLayer.zoomBounds = + zoomBounds; } - } else { + } else if (type === '_staticTileLayer') { + if (this[type].layerBounds) { + if (!bounds) { + bounds = this[type].layerBounds; + zoomBounds = this[type].zoomBounds; + } else { + bounds.extend(this[type].layerBounds.min); + bounds.extend(this[type].layerBounds.max); + } + } + } else if (type === '_imageLayer') { + if (this[type].layerBounds) { + if (!bounds) { + bounds = this[type].layerBounds; + zoomBounds = this[type].zoomBounds; + } else { + bounds.extend(this[type].layerBounds.min); + bounds.extend(this[type].layerBounds.max); + } + } + } else if ( + // only process extent if mapmlvectors is not empty + type === '_mapmlvectors' && + Object.keys(this[type]._layers).length !== 0 + ) { if (this[type].layerBounds) { if (!bounds) { bounds = this[type].layerBounds; @@ -458,7 +395,11 @@ export var MapMLLayer = L.Layer.extend({ if (bounds) { //assigns the formatted extent object to .extent and spreads the zoom ranges to .extent also this._layerEl.extent = Object.assign( - M._convertAndFormatPCRS(bounds, this._map), + M._convertAndFormatPCRS( + bounds, + this._properties.crs, + this._properties.projection + ), { zoom: zoomBounds } ); } @@ -473,10 +414,10 @@ export var MapMLLayer = L.Layer.extend({ }, redraw: function () { // for now, only redraw templated layers. - if (this._extent._mapExtents) { - for (let i = 0; i < this._extent._mapExtents.length; i++) { - if (this._extent._mapExtents[i].templatedLayer) { - this._extent._mapExtents[i].templatedLayer.redraw(); + if (this._properties._mapExtents) { + for (let i = 0; i < this._properties._mapExtents.length; i++) { + if (this._properties._mapExtents[i].templatedLayer) { + this._properties._mapExtents[i].templatedLayer.redraw(); } } } @@ -491,8 +432,10 @@ export var MapMLLayer = L.Layer.extend({ // get the min and max zooms from all extents var toZoom = e.zoom, zoom = - this._extent && this._extent._mapExtents - ? this._extent._mapExtents[0].querySelector('map-input[type=zoom]') + this._properties && this._properties._mapExtents + ? this._properties._mapExtents[0].querySelector( + 'map-input[type=zoom]' + ) : null, min = zoom && zoom.hasAttribute('min') @@ -503,8 +446,8 @@ export var MapMLLayer = L.Layer.extend({ ? parseInt(zoom.getAttribute('max')) : this._map.getMaxZoom(); if (zoom) { - for (let i = 1; i < this._extent._mapExtents.length; i++) { - zoom = this._extent._mapExtents[i].querySelector( + for (let i = 1; i < this._properties._mapExtents.length; i++) { + zoom = this._properties._mapExtents[i].querySelector( 'map-input[type=zoom]' ); if (zoom && zoom.hasAttribute('min')) { @@ -516,23 +459,23 @@ export var MapMLLayer = L.Layer.extend({ } } var canZoom = - (toZoom < min && this._extent.zoomout) || - (toZoom > max && this._extent.zoomin); + (toZoom < min && this._properties.zoomout) || + (toZoom > max && this._properties.zoomin); if (!(min <= toZoom && toZoom <= max)) { - if (this._extent.zoomin && toZoom > max) { + if (this._properties.zoomin && toZoom > max) { // this._href is the 'original' url from which this layer came // since we are following a zoom link we will be getting a new // layer almost, resetting child content as appropriate - this._href = this._extent.zoomin; - this._layerEl.src = this._extent.zoomin; + this._href = this._properties.zoomin; + this._layerEl.src = this._properties.zoomin; // this.href is the "public" property. When a dynamic layer is // accessed, this value changes with every new extent received - this.href = this._extent.zoomin; - this._layerEl.src = this._extent.zoomin; - } else if (this._extent.zoomout && toZoom < min) { - this._href = this._extent.zoomout; - this.href = this._extent.zoomout; - this._layerEl.src = this._extent.zoomout; + this.href = this._properties.zoomin; + this._layerEl.src = this._properties.zoomin; + } else if (this._properties.zoomout && toZoom < min) { + this._href = this._properties.zoomout; + this.href = this._properties.zoomout; + this._layerEl.src = this._properties.zoomout; } } if (this._templatedLayer && canZoom) { @@ -545,7 +488,8 @@ export var MapMLLayer = L.Layer.extend({ if (this._staticTileLayer) map.removeLayer(this._staticTileLayer); if (this._mapmlvectors) map.removeLayer(this._mapmlvectors); if (this._imageLayer) map.removeLayer(this._imageLayer); - if (this._extent && this._extent._mapExtents) this._removeExtents(map); + if (this._properties && this._properties._mapExtents) + this._removeExtents(map); map.fire('checkdisabled'); map.off('popupopen', this._attachSkipButtons); @@ -553,587 +497,349 @@ export var MapMLLayer = L.Layer.extend({ getAttribution: function () { return this.options.attribution; }, + getLayerUserControlsHTML: function () { + return this._mapmlLayerItem + ? this._mapmlLayerItem + : this._createLayerControlHTML(); + }, + _createLayerControlHTML: function () { + if (!this._mapmlLayerItem) { + var fieldset = L.DomUtil.create('fieldset', 'mapml-layer-item'), + input = L.DomUtil.create('input'), + layerItemName = L.DomUtil.create('span', 'mapml-layer-item-name'), + settingsButtonNameIcon = L.DomUtil.create('span'), + layerItemProperty = L.DomUtil.create( + 'div', + 'mapml-layer-item-properties', + fieldset + ), + layerItemSettings = L.DomUtil.create( + 'div', + 'mapml-layer-item-settings', + fieldset + ), + itemToggleLabel = L.DomUtil.create( + 'label', + 'mapml-layer-item-toggle', + layerItemProperty + ), + layerItemControls = L.DomUtil.create( + 'div', + 'mapml-layer-item-controls', + layerItemProperty + ), + opacityControl = L.DomUtil.create( + 'details', + 'mapml-layer-item-opacity mapml-control-layers', + layerItemSettings + ), + opacity = L.DomUtil.create('input'), + opacityControlSummary = L.DomUtil.create('summary'), + svgSettingsControlIcon = L.SVG.create('svg'), + settingsControlPath1 = L.SVG.create('path'), + settingsControlPath2 = L.SVG.create('path'), + extentsFieldset = L.DomUtil.create( + 'fieldset', + 'mapml-layer-grouped-extents' + ), + mapEl = this._layerEl.parentNode; + this.opacityEl = opacity; + this._mapmlLayerItem = fieldset; + + // append the paths in svg for the remove layer and toggle icons + svgSettingsControlIcon.setAttribute('viewBox', '0 0 24 24'); + svgSettingsControlIcon.setAttribute('height', '22'); + svgSettingsControlIcon.setAttribute('width', '22'); + svgSettingsControlIcon.setAttribute('fill', 'currentColor'); + settingsControlPath1.setAttribute('d', 'M0 0h24v24H0z'); + settingsControlPath1.setAttribute('fill', 'none'); + settingsControlPath2.setAttribute( + 'd', + 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' + ); + svgSettingsControlIcon.appendChild(settingsControlPath1); + svgSettingsControlIcon.appendChild(settingsControlPath2); - getLayerExtentHTML: function (labelName, i) { - var extent = L.DomUtil.create('fieldset', 'mapml-layer-extent'), - extentProperties = L.DomUtil.create( - 'div', - 'mapml-layer-item-properties', - extent - ), - extentSettings = L.DomUtil.create( - 'div', - 'mapml-layer-item-settings', - extent - ), - extentLabel = L.DomUtil.create( - 'label', - 'mapml-layer-item-toggle', - extentProperties - ), - input = L.DomUtil.create('input'), - svgExtentControlIcon = L.SVG.create('svg'), - extentControlPath1 = L.SVG.create('path'), - extentControlPath2 = L.SVG.create('path'), - extentNameIcon = L.DomUtil.create('span'), - extentItemControls = L.DomUtil.create( - 'div', - 'mapml-layer-item-controls', - extentProperties - ), - opacityControl = L.DomUtil.create( - 'details', - 'mapml-layer-item-opacity', - extentSettings - ), - extentOpacitySummary = L.DomUtil.create('summary', '', opacityControl), - mapEl = this._layerEl.parentNode, - layerEl = this._layerEl, - opacity = L.DomUtil.create('input', '', opacityControl); - extentSettings.hidden = true; - extent.setAttribute('aria-grabbed', 'false'); - if (!labelName) { - // if a label attribute is not present, set it to hidden in layer control - extent.setAttribute('hidden', ''); - this._extent._mapExtents[i].hidden = true; - } - - // append the svg paths - svgExtentControlIcon.setAttribute('viewBox', '0 0 24 24'); - svgExtentControlIcon.setAttribute('height', '22'); - svgExtentControlIcon.setAttribute('width', '22'); - extentControlPath1.setAttribute('d', 'M0 0h24v24H0z'); - extentControlPath1.setAttribute('fill', 'none'); - extentControlPath2.setAttribute( - 'd', - 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' - ); - svgExtentControlIcon.appendChild(extentControlPath1); - svgExtentControlIcon.appendChild(extentControlPath2); - - let removeExtentButton = L.DomUtil.create( - 'button', - 'mapml-layer-item-remove-control', - extentItemControls - ); - removeExtentButton.type = 'button'; - removeExtentButton.title = 'Remove Sub Layer'; - removeExtentButton.innerHTML = ""; - removeExtentButton.classList.add('mapml-button'); - L.DomEvent.on(removeExtentButton, 'click', L.DomEvent.stop); - L.DomEvent.on( - removeExtentButton, - 'click', - (e) => { - let allRemoved = true; - e.target.checked = false; - this._extent._mapExtents[i].removed = true; - this._extent._mapExtents[i].checked = false; - if (this._layerEl.checked) - this._changeExtent(e, this._extent._mapExtents[i]); - this._extent._mapExtents[i].extentAnatomy.parentNode.removeChild( - this._extent._mapExtents[i].extentAnatomy - ); - for (let j = 0; j < this._extent._mapExtents.length; j++) { - if (!this._extent._mapExtents[j].removed) allRemoved = false; - } - if (allRemoved) - this._layerItemSettingsHTML.removeChild(this._extentGroupAnatomy); - }, - this - ); - - let extentsettingsButton = L.DomUtil.create( - 'button', - 'mapml-layer-item-settings-control', - extentItemControls - ); - extentsettingsButton.type = 'button'; - extentsettingsButton.title = 'Extent Settings'; - extentsettingsButton.setAttribute('aria-expanded', false); - extentsettingsButton.classList.add('mapml-button'); - L.DomEvent.on( - extentsettingsButton, - 'click', - (e) => { - if (extentSettings.hidden === true) { - extentsettingsButton.setAttribute('aria-expanded', true); - extentSettings.hidden = false; - } else { - extentsettingsButton.setAttribute('aria-expanded', false); - extentSettings.hidden = true; - } - }, - this - ); - - extentNameIcon.setAttribute('aria-hidden', true); - extentLabel.appendChild(input); - extentsettingsButton.appendChild(extentNameIcon); - extentNameIcon.appendChild(svgExtentControlIcon); - extentOpacitySummary.innerText = 'Opacity'; - extentOpacitySummary.id = - 'mapml-layer-item-opacity-' + L.stamp(extentOpacitySummary); - opacity.setAttribute('type', 'range'); - opacity.setAttribute('min', '0'); - opacity.setAttribute('max', '1.0'); - opacity.setAttribute('step', '0.1'); - opacity.setAttribute( - 'aria-labelledby', - 'mapml-layer-item-opacity-' + L.stamp(extentOpacitySummary) - ); - let opacityValue = this._extent._mapExtents[i].hasAttribute('opacity') - ? this._extent._mapExtents[i].getAttribute('opacity') - : '1.0'; - this._extent._mapExtents[i]._templateVars.opacity = opacityValue; - opacity.setAttribute('value', opacityValue); - opacity.value = opacityValue; - L.DomEvent.on( - opacity, - 'change', - this._changeExtentOpacity, - this._extent._mapExtents[i] - ); + layerItemSettings.hidden = true; + settingsButtonNameIcon.setAttribute('aria-hidden', true); - var extentItemNameSpan = L.DomUtil.create( - 'span', - 'mapml-layer-item-name', - extentLabel - ); - input.defaultChecked = this._extent._mapExtents[i] ? true : false; - this._extent._mapExtents[i].checked = input.defaultChecked; - input.type = 'checkbox'; - extentItemNameSpan.innerHTML = labelName; - L.DomEvent.on(input, 'change', (e) => { - this._changeExtent(e, this._extent._mapExtents[i]); - }); - extentItemNameSpan.id = - 'mapml-extent-item-name-{' + L.stamp(extentItemNameSpan) + '}'; - extent.setAttribute('aria-labelledby', extentItemNameSpan.id); - extentItemNameSpan.extent = this._extent._mapExtents[i]; - - extent.ontouchstart = extent.onmousedown = (downEvent) => { - if ( - (downEvent.target.parentElement.tagName.toLowerCase() === 'label' && - downEvent.target.tagName.toLowerCase() !== 'input') || - downEvent.target.tagName.toLowerCase() === 'label' - ) { - downEvent.stopPropagation(); - downEvent = - downEvent instanceof TouchEvent ? downEvent.touches[0] : downEvent; - - let control = extent, - controls = extent.parentNode, - moving = false, - yPos = downEvent.clientY; - - document.body.ontouchmove = document.body.onmousemove = (moveEvent) => { - moveEvent.preventDefault(); - moveEvent = - moveEvent instanceof TouchEvent ? moveEvent.touches[0] : moveEvent; - - // Fixes flickering by only moving element when there is enough space - let offset = moveEvent.clientY - yPos; - moving = Math.abs(offset) > 5 || moving; + let removeControlButton = L.DomUtil.create( + 'button', + 'mapml-layer-item-remove-control', + layerItemControls + ); + removeControlButton.type = 'button'; + removeControlButton.title = 'Remove Layer'; + removeControlButton.innerHTML = + ""; + removeControlButton.classList.add('mapml-button'); + //L.DomEvent.disableClickPropagation(removeControlButton); + L.DomEvent.on(removeControlButton, 'click', L.DomEvent.stop); + L.DomEvent.on( + removeControlButton, + 'click', + (e) => { + let fieldset = 0, + elem, + root; + root = + mapEl.tagName === 'MAPML-VIEWER' + ? mapEl.shadowRoot + : mapEl.querySelector('.mapml-web-map').shadowRoot; if ( - (controls && !moving) || - (controls && controls.childElementCount <= 1) || - controls.getBoundingClientRect().top > - control.getBoundingClientRect().bottom || - controls.getBoundingClientRect().bottom < - control.getBoundingClientRect().top + e.target.closest('fieldset').nextElementSibling && + !e.target.closest('fieldset').nextElementSibling.disbaled ) { - return; - } - - controls.classList.add('mapml-draggable'); - control.style.transform = 'translateY(' + offset + 'px)'; - control.style.pointerEvents = 'none'; - - let x = moveEvent.clientX, - y = moveEvent.clientY, - root = - mapEl.tagName === 'MAPML-VIEWER' - ? mapEl.shadowRoot - : mapEl.querySelector('.mapml-web-map').shadowRoot, - elementAt = root.elementFromPoint(x, y), - swapControl = - !elementAt || !elementAt.closest('fieldset') - ? control - : elementAt.closest('fieldset'); - - swapControl = - Math.abs(offset) <= swapControl.offsetHeight - ? control - : swapControl; - - control.setAttribute('aria-grabbed', 'true'); - control.setAttribute('aria-dropeffect', 'move'); - if (swapControl && controls === swapControl.parentNode) { - swapControl = - swapControl !== control.nextSibling - ? swapControl - : swapControl.nextSibling; - if (control !== swapControl) { - yPos = moveEvent.clientY; - control.style.transform = null; + elem = e.target.closest('fieldset').previousElementSibling; + while (elem) { + fieldset += 2; // find the next layer menu item + elem = elem.previousElementSibling; } - controls.insertBefore(control, swapControl); + } else { + // focus on the link + elem = 'link'; } - }; + mapEl.removeChild( + e.target.closest('fieldset').querySelector('span').layer._layerEl + ); + elem = elem + ? root.querySelector('.leaflet-control-attribution') + .firstElementChild + : (elem = root.querySelectorAll('input')[fieldset]); + elem.focus(); + }, + this + ); - document.body.ontouchend = document.body.onmouseup = () => { - control.setAttribute('aria-grabbed', 'false'); - control.removeAttribute('aria-dropeffect'); - control.style.pointerEvents = null; - control.style.transform = null; - let controlsElems = controls.children, - zIndex = 0; - for (let c of controlsElems) { - let extentEl = c.querySelector('span').extent; - - extentEl.setAttribute('data-moving', ''); - layerEl.insertAdjacentElement('beforeend', extentEl); - extentEl.removeAttribute('data-moving'); - - extentEl.extentZIndex = zIndex; - extentEl.templatedLayer.setZIndex(zIndex); - zIndex++; + let itemSettingControlButton = L.DomUtil.create( + 'button', + 'mapml-layer-item-settings-control', + layerItemControls + ); + itemSettingControlButton.type = 'button'; + itemSettingControlButton.title = 'Layer Settings'; + itemSettingControlButton.setAttribute('aria-expanded', false); + itemSettingControlButton.classList.add('mapml-button'); + L.DomEvent.on( + itemSettingControlButton, + 'click', + (e) => { + let layerControl = this._layerEl._layerControl._container; + if (!layerControl._isExpanded && e.pointerType === 'touch') { + layerControl._isExpanded = true; + return; } - controls.classList.remove('mapml-draggable'); - document.body.ontouchmove = - document.body.onmousemove = - document.body.ontouchend = - document.body.onmouseup = - null; - }; - } - }; - return extent; - }, - - getLayerUserControlsHTML: function () { - var fieldset = L.DomUtil.create('fieldset', 'mapml-layer-item'), - input = L.DomUtil.create('input'), - layerItemName = L.DomUtil.create('span', 'mapml-layer-item-name'), - settingsButtonNameIcon = L.DomUtil.create('span'), - layerItemProperty = L.DomUtil.create( - 'div', - 'mapml-layer-item-properties', - fieldset - ), - layerItemSettings = L.DomUtil.create( - 'div', - 'mapml-layer-item-settings', - fieldset - ), - itemToggleLabel = L.DomUtil.create( - 'label', - 'mapml-layer-item-toggle', - layerItemProperty - ), - layerItemControls = L.DomUtil.create( - 'div', - 'mapml-layer-item-controls', - layerItemProperty - ), - opacityControl = L.DomUtil.create( - 'details', - 'mapml-layer-item-opacity mapml-control-layers', - layerItemSettings - ), - opacity = L.DomUtil.create('input'), - opacityControlSummary = L.DomUtil.create('summary'), - svgSettingsControlIcon = L.SVG.create('svg'), - settingsControlPath1 = L.SVG.create('path'), - settingsControlPath2 = L.SVG.create('path'), - extentsFieldset = L.DomUtil.create( - 'fieldset', - 'mapml-layer-grouped-extents' - ), - mapEl = this._layerEl.parentNode; - this.opacityEl = opacity; - this._mapmlLayerItem = fieldset; - - // append the paths in svg for the remove layer and toggle icons - svgSettingsControlIcon.setAttribute('viewBox', '0 0 24 24'); - svgSettingsControlIcon.setAttribute('height', '22'); - svgSettingsControlIcon.setAttribute('width', '22'); - svgSettingsControlIcon.setAttribute('fill', 'currentColor'); - settingsControlPath1.setAttribute('d', 'M0 0h24v24H0z'); - settingsControlPath1.setAttribute('fill', 'none'); - settingsControlPath2.setAttribute( - 'd', - 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' - ); - svgSettingsControlIcon.appendChild(settingsControlPath1); - svgSettingsControlIcon.appendChild(settingsControlPath2); - - layerItemSettings.hidden = true; - settingsButtonNameIcon.setAttribute('aria-hidden', true); + if (layerItemSettings.hidden === true) { + itemSettingControlButton.setAttribute('aria-expanded', true); + layerItemSettings.hidden = false; + } else { + itemSettingControlButton.setAttribute('aria-expanded', false); + layerItemSettings.hidden = true; + } + }, + this + ); - let removeControlButton = L.DomUtil.create( - 'button', - 'mapml-layer-item-remove-control', - layerItemControls - ); - removeControlButton.type = 'button'; - removeControlButton.title = 'Remove Layer'; - removeControlButton.innerHTML = ""; - removeControlButton.classList.add('mapml-button'); - //L.DomEvent.disableClickPropagation(removeControlButton); - L.DomEvent.on(removeControlButton, 'click', L.DomEvent.stop); - L.DomEvent.on( - removeControlButton, - 'click', - (e) => { - let fieldset = 0, - elem, - root; - root = - mapEl.tagName === 'MAPML-VIEWER' - ? mapEl.shadowRoot - : mapEl.querySelector('.mapml-web-map').shadowRoot; + input.defaultChecked = this._map ? true : false; + input.type = 'checkbox'; + input.setAttribute('class', 'leaflet-control-layers-selector'); + layerItemName.layer = this; + + if (this._legendUrl) { + var legendLink = document.createElement('a'); + legendLink.text = ' ' + this._title; + legendLink.href = this._legendUrl; + legendLink.target = '_blank'; + legendLink.draggable = false; + layerItemName.appendChild(legendLink); + } else { + layerItemName.innerHTML = this._title; + } + layerItemName.id = + 'mapml-layer-item-name-{' + L.stamp(layerItemName) + '}'; + opacityControlSummary.innerText = 'Opacity'; + opacityControlSummary.id = + 'mapml-layer-item-opacity-' + L.stamp(opacityControlSummary); + opacityControl.appendChild(opacityControlSummary); + opacityControl.appendChild(opacity); + opacity.setAttribute('type', 'range'); + opacity.setAttribute('min', '0'); + opacity.setAttribute('max', '1.0'); + opacity.setAttribute('value', this._container.style.opacity || '1.0'); + opacity.setAttribute('step', '0.1'); + opacity.setAttribute('aria-labelledby', opacityControlSummary.id); + opacity.value = this._container.style.opacity || '1.0'; + + fieldset.setAttribute('aria-grabbed', 'false'); + fieldset.setAttribute('aria-labelledby', layerItemName.id); + + fieldset.ontouchstart = fieldset.onmousedown = (downEvent) => { if ( - e.target.closest('fieldset').nextElementSibling && - !e.target.closest('fieldset').nextElementSibling.disbaled + (downEvent.target.parentElement.tagName.toLowerCase() === 'label' && + downEvent.target.tagName.toLowerCase() !== 'input') || + downEvent.target.tagName.toLowerCase() === 'label' ) { - elem = e.target.closest('fieldset').previousElementSibling; - while (elem) { - fieldset += 2; // find the next layer menu item - elem = elem.previousElementSibling; - } - } else { - // focus on the link - elem = 'link'; - } - mapEl.removeChild( - e.target.closest('fieldset').querySelector('span').layer._layerEl - ); - elem = elem - ? root.querySelector('.leaflet-control-attribution').firstElementChild - : (elem = root.querySelectorAll('input')[fieldset]); - elem.focus(); - }, - this - ); - - let itemSettingControlButton = L.DomUtil.create( - 'button', - 'mapml-layer-item-settings-control', - layerItemControls - ); - itemSettingControlButton.type = 'button'; - itemSettingControlButton.title = 'Layer Settings'; - itemSettingControlButton.setAttribute('aria-expanded', false); - itemSettingControlButton.classList.add('mapml-button'); - L.DomEvent.on( - itemSettingControlButton, - 'click', - (e) => { - let layerControl = this._layerEl._layerControl._container; - if (!layerControl._isExpanded && e.pointerType === 'touch') { - layerControl._isExpanded = true; - return; - } - if (layerItemSettings.hidden === true) { - itemSettingControlButton.setAttribute('aria-expanded', true); - layerItemSettings.hidden = false; - } else { - itemSettingControlButton.setAttribute('aria-expanded', false); - layerItemSettings.hidden = true; - } - }, - this - ); + downEvent = + downEvent instanceof TouchEvent ? downEvent.touches[0] : downEvent; + let control = fieldset, + controls = fieldset.parentNode, + moving = false, + yPos = downEvent.clientY; + + document.body.ontouchmove = document.body.onmousemove = ( + moveEvent + ) => { + moveEvent.preventDefault(); + moveEvent = + moveEvent instanceof TouchEvent + ? moveEvent.touches[0] + : moveEvent; + + // Fixes flickering by only moving element when there is enough space + let offset = moveEvent.clientY - yPos; + moving = Math.abs(offset) > 5 || moving; + if ( + (controls && !moving) || + (controls && controls.childElementCount <= 1) || + controls.getBoundingClientRect().top > + control.getBoundingClientRect().bottom || + controls.getBoundingClientRect().bottom < + control.getBoundingClientRect().top + ) { + return; + } - input.defaultChecked = this._map ? true : false; - input.type = 'checkbox'; - input.setAttribute('class', 'leaflet-control-layers-selector'); - layerItemName.layer = this; - - if (this._legendUrl) { - var legendLink = document.createElement('a'); - legendLink.text = ' ' + this._title; - legendLink.href = this._legendUrl; - legendLink.target = '_blank'; - legendLink.draggable = false; - layerItemName.appendChild(legendLink); - } else { - layerItemName.innerHTML = this._title; - } - layerItemName.id = 'mapml-layer-item-name-{' + L.stamp(layerItemName) + '}'; - opacityControlSummary.innerText = 'Opacity'; - opacityControlSummary.id = - 'mapml-layer-item-opacity-' + L.stamp(opacityControlSummary); - opacityControl.appendChild(opacityControlSummary); - opacityControl.appendChild(opacity); - opacity.setAttribute('type', 'range'); - opacity.setAttribute('min', '0'); - opacity.setAttribute('max', '1.0'); - opacity.setAttribute('value', this._container.style.opacity || '1.0'); - opacity.setAttribute('step', '0.1'); - opacity.setAttribute('aria-labelledby', opacityControlSummary.id); - opacity.value = this._container.style.opacity || '1.0'; - - fieldset.setAttribute('aria-grabbed', 'false'); - fieldset.setAttribute('aria-labelledby', layerItemName.id); - - fieldset.ontouchstart = fieldset.onmousedown = (downEvent) => { - if ( - (downEvent.target.parentElement.tagName.toLowerCase() === 'label' && - downEvent.target.tagName.toLowerCase() !== 'input') || - downEvent.target.tagName.toLowerCase() === 'label' - ) { - downEvent = - downEvent instanceof TouchEvent ? downEvent.touches[0] : downEvent; - let control = fieldset, - controls = fieldset.parentNode, - moving = false, - yPos = downEvent.clientY; - - document.body.ontouchmove = document.body.onmousemove = (moveEvent) => { - moveEvent.preventDefault(); - moveEvent = - moveEvent instanceof TouchEvent ? moveEvent.touches[0] : moveEvent; - - // Fixes flickering by only moving element when there is enough space - let offset = moveEvent.clientY - yPos; - moving = Math.abs(offset) > 5 || moving; - if ( - (controls && !moving) || - (controls && controls.childElementCount <= 1) || - controls.getBoundingClientRect().top > - control.getBoundingClientRect().bottom || - controls.getBoundingClientRect().bottom < - control.getBoundingClientRect().top - ) { - return; - } + controls.classList.add('mapml-draggable'); + control.style.transform = 'translateY(' + offset + 'px)'; + control.style.pointerEvents = 'none'; + + let x = moveEvent.clientX, + y = moveEvent.clientY, + root = + mapEl.tagName === 'MAPML-VIEWER' + ? mapEl.shadowRoot + : mapEl.querySelector('.mapml-web-map').shadowRoot, + elementAt = root.elementFromPoint(x, y), + swapControl = + !elementAt || !elementAt.closest('fieldset') + ? control + : elementAt.closest('fieldset'); - controls.classList.add('mapml-draggable'); - control.style.transform = 'translateY(' + offset + 'px)'; - control.style.pointerEvents = 'none'; - - let x = moveEvent.clientX, - y = moveEvent.clientY, - root = - mapEl.tagName === 'MAPML-VIEWER' - ? mapEl.shadowRoot - : mapEl.querySelector('.mapml-web-map').shadowRoot, - elementAt = root.elementFromPoint(x, y), swapControl = - !elementAt || !elementAt.closest('fieldset') + Math.abs(offset) <= swapControl.offsetHeight ? control - : elementAt.closest('fieldset'); - - swapControl = - Math.abs(offset) <= swapControl.offsetHeight - ? control - : swapControl; - - control.setAttribute('aria-grabbed', 'true'); - control.setAttribute('aria-dropeffect', 'move'); - if (swapControl && controls === swapControl.parentNode) { - swapControl = - swapControl !== control.nextSibling - ? swapControl - : swapControl.nextSibling; - if (control !== swapControl) { - yPos = moveEvent.clientY; - control.style.transform = null; + : swapControl; + + control.setAttribute('aria-grabbed', 'true'); + control.setAttribute('aria-dropeffect', 'move'); + if (swapControl && controls === swapControl.parentNode) { + swapControl = + swapControl !== control.nextSibling + ? swapControl + : swapControl.nextSibling; + if (control !== swapControl) { + yPos = moveEvent.clientY; + control.style.transform = null; + } + controls.insertBefore(control, swapControl); } - controls.insertBefore(control, swapControl); - } - }; + }; - document.body.ontouchend = document.body.onmouseup = () => { - control.setAttribute('aria-grabbed', 'false'); - control.removeAttribute('aria-dropeffect'); - control.style.pointerEvents = null; - control.style.transform = null; - let controlsElems = controls.children, - zIndex = 1; - for (let c of controlsElems) { - let layerEl = c.querySelector('span').layer._layerEl; - - layerEl.setAttribute('data-moving', ''); - mapEl.insertAdjacentElement('beforeend', layerEl); - layerEl.removeAttribute('data-moving'); - - layerEl._layer.setZIndex(zIndex); - zIndex++; - } - controls.classList.remove('mapml-draggable'); - document.body.ontouchmove = - document.body.onmousemove = - document.body.onmouseup = - null; - }; - } - }; + document.body.ontouchend = document.body.onmouseup = () => { + control.setAttribute('aria-grabbed', 'false'); + control.removeAttribute('aria-dropeffect'); + control.style.pointerEvents = null; + control.style.transform = null; + let controlsElems = controls.children, + zIndex = 1; + // re-order layer elements DOM order + for (let c of controlsElems) { + let layerEl = c.querySelector('span').layer._layerEl; + layerEl.setAttribute('data-moving', ''); + mapEl.insertAdjacentElement('beforeend', layerEl); + layerEl.removeAttribute('data-moving'); + } + // update zIndex of all layer- elements + let layers = mapEl.querySelectorAll('layer-'); + for (let i = 0; i < layers.length; i++) { + let layer = layers[i]._layer; + if (layer.options.zIndex !== zIndex) { + layer.setZIndex(zIndex); + } + zIndex++; + } + controls.classList.remove('mapml-draggable'); + document.body.ontouchmove = + document.body.onmousemove = + document.body.onmouseup = + null; + }; + } + }; - L.DomEvent.on(opacity, 'change', this._changeOpacity, this); + L.DomEvent.on(opacity, 'change', this._changeOpacity, this); - itemToggleLabel.appendChild(input); - itemToggleLabel.appendChild(layerItemName); - itemSettingControlButton.appendChild(settingsButtonNameIcon); - settingsButtonNameIcon.appendChild(svgSettingsControlIcon); + itemToggleLabel.appendChild(input); + itemToggleLabel.appendChild(layerItemName); + itemSettingControlButton.appendChild(settingsButtonNameIcon); + settingsButtonNameIcon.appendChild(svgSettingsControlIcon); - if (this._styles) { - layerItemSettings.appendChild(this._styles); - } + if (this._styles) { + layerItemSettings.appendChild(this._styles); + } - if (this._userInputs) { - var frag = document.createDocumentFragment(); - var templates = this._extent._templateVars; - if (templates) { - for (var i = 0; i < templates.length; i++) { - var template = templates[i]; - for (var j = 0; j < template.values.length; j++) { - var mapmlInput = template.values[j], - id = '#' + mapmlInput.getAttribute('id'); - // don't add it again if it is referenced > once - if ( - mapmlInput.tagName.toLowerCase() === 'map-select' && - !frag.querySelector(id) - ) { - // generate a
- var selectdetails = L.DomUtil.create( - 'details', - 'mapml-layer-item-time mapml-control-layers', - frag - ), - selectsummary = L.DomUtil.create('summary'), - selectSummaryLabel = L.DomUtil.create('label'); - selectSummaryLabel.innerText = mapmlInput.getAttribute('name'); - selectSummaryLabel.setAttribute( - 'for', - mapmlInput.getAttribute('id') - ); - selectsummary.appendChild(selectSummaryLabel); - selectdetails.appendChild(selectsummary); - selectdetails.appendChild(mapmlInput.htmlselect); + if (this._userInputs) { + var frag = document.createDocumentFragment(); + var templates = this._properties._templateVars; + if (templates) { + for (var i = 0; i < templates.length; i++) { + var template = templates[i]; + for (var j = 0; j < template.values.length; j++) { + var mapmlInput = template.values[j], + id = '#' + mapmlInput.getAttribute('id'); + // don't add it again if it is referenced > once + if ( + mapmlInput.tagName.toLowerCase() === 'map-select' && + !frag.querySelector(id) + ) { + // generate a
+ var selectdetails = L.DomUtil.create( + 'details', + 'mapml-layer-item-time mapml-control-layers', + frag + ), + selectsummary = L.DomUtil.create('summary'), + selectSummaryLabel = L.DomUtil.create('label'); + selectSummaryLabel.innerText = mapmlInput.getAttribute('name'); + selectSummaryLabel.setAttribute( + 'for', + mapmlInput.getAttribute('id') + ); + selectsummary.appendChild(selectSummaryLabel); + selectdetails.appendChild(selectsummary); + selectdetails.appendChild(mapmlInput.htmlselect); + } } } } + layerItemSettings.appendChild(frag); } - layerItemSettings.appendChild(frag); - } - // if there are extents, add them to the layer control - if (this._extent && this._extent._mapExtents) { - var allHidden = true; - this._layerItemSettingsHTML = layerItemSettings; - this._extentGroupAnatomy = extentsFieldset; - extentsFieldset.setAttribute('aria-label', 'Sublayers'); - for (let j = 0; j < this._extent._mapExtents.length; j++) { - extentsFieldset.appendChild(this._extent._mapExtents[j].extentAnatomy); - if (!this._extent._mapExtents[j].hidden) allHidden = false; + // if there are extents, add them to the layer control + if (this._properties && this._properties._mapExtents) { + var allHidden = true; + this._layerItemSettingsHTML = layerItemSettings; + this._propertiesGroupAnatomy = extentsFieldset; + extentsFieldset.setAttribute('aria-label', 'Sublayers'); + for (let j = 0; j < this._properties._mapExtents.length; j++) { + extentsFieldset.appendChild( + this._properties._mapExtents[j].extentAnatomy + ); + if (!this._properties._mapExtents[j].hidden) allHidden = false; + } + if (!allHidden) layerItemSettings.appendChild(extentsFieldset); } - if (!allHidden) layerItemSettings.appendChild(extentsFieldset); } - return this._mapmlLayerItem; }, _initialize: function (content) { @@ -1145,425 +851,680 @@ export var MapMLLayer = L.Layer.extend({ // content of the element, but if no this._href / src is provided // but there *is* child content of the element (which is copied/ // referred to by this._content), we should use that content. - if (this._href) { - var xhr = new XMLHttpRequest(); - // xhr.withCredentials = true; - _get(this._href, _processInitialExtent); - } else if (content) { - // may not set this._extent if it can't be done from the content - // (eg a single point) and there's no map to provide a default yet - _processInitialExtent.call(this, content); - } - function _get(url, fCallback) { - xhr.onreadystatechange = function () { - if (this.readyState === this.DONE) { + _processContent.call(this, content, this._href ? false : true); + function _processContent(mapml, local) { + var base = new URL( + mapml.querySelector('map-base') + ? mapml.querySelector('map-base').getAttribute('href') + : local + ? mapml.baseURI + : layer._href, + layer._href + ).href; + layer._properties = {}; + // sets layer._properties.projection + determineLayerProjection(); + // requires that layer._properties.projection be set + if (selectMatchingAlternateProjection()) return; + // set layer._properties._mapExtents and layer._properties._templateVars + if (layer._properties.crs) processExtents(); + layer._styles = getAlternateStyles(); + parseLicenseAndLegend(); + setLayerTitle(); + setZoomInOrOutLinks(); + // crs is only set if the layer has the same projection as the map + if (layer._properties.crs) processTiles(); + processFeatures(); + M._parseStylesheetAsHTML(mapml, base, layer._container); + layer._validateExtent(); + copyRemoteContentToShadowRoot(); + // update controls if needed based on mapml-viewer controls/controlslist attribute + if (layer._layerEl.parentElement) { + // if layer does not have a parent Element, do not need to set Controls + layer._layerEl.parentElement._toggleControls(); + } + // local functions + // sets layer._properties.projection. Supposed to replace / simplify + // the dependencies on convoluted getProjection() interface, but doesn't quite + // succeed, yet. + function determineLayerProjection() { + let projection = layer.options.mapprojection; + if (mapml.querySelector('map-meta[name=projection][content]')) { + projection = + M._metaContentToObject( + mapml + .querySelector('map-meta[name=projection]') + .getAttribute('content') + ).content || projection; + } else if (mapml.querySelector('map-extent[units]')) { + const getProjectionFrom = (extents) => { + let extentProj = extents[0].attributes.units.value; + let isMatch = true; + for (let i = 0; i < extents.length; i++) { + if (extentProj !== extents[i].attributes.units.value) { + isMatch = false; + } + } + return isMatch ? extentProj : null; + }; + projection = + getProjectionFrom( + Array.from(mapml.querySelectorAll('map-extent[units]')) + ) || projection; + } else { + console.log( + `A projection was not assigned to the '${layer._layerEl.label}' Layer. Please specify a projection for that layer using a map-meta element. See more here - https://maps4html.org/web-map-doc/docs/elements/meta/` + ); + } + layer._properties.projection = projection; + if (layer._properties.projection === layer.options.mapprojection) { + layer._properties.crs = M[layer._properties.projection]; + } + } + // determine if, where there's no match of the current layer's projection + // and that of the map, if there is a linked alternate text/mapml + // resource that matches the map's projection + function selectMatchingAlternateProjection() { + let selectedAlternate = + layer._properties.projection !== layer.options.mapprojection && + mapml.querySelector( + 'map-head map-link[rel=alternate][projection=' + + layer.options.mapprojection + + '][href]' + ); + try { + if (selectedAlternate) { + let url = new URL(selectedAlternate.getAttribute('href'), base) + .href; + layer._layerEl.dispatchEvent( + new CustomEvent('changeprojection', { + detail: { + href: url + } + }) + ); + return true; + //if this is the only layer, but the projection doesn't match, + // set the map's projection to that of the layer + } else if ( + layer._properties.projection !== layer.options.mapprojection && + layer._layerEl.parentElement.layers.length === 1 + ) { + layer._layerEl.parentElement.projection = + layer._properties.projection; + return true; + } + } catch (error) {} + return false; + } + // initialize layer._properties._mapExtents (and associated/derived/convenience property _templateVars + function processExtents() { + let projectionMatch = + layer._properties.projection === layer.options.mapprojection; + let extents = mapml.querySelectorAll('map-extent[units]'); + if (extents.length === 0) { + return; + } + layer._properties._mapExtents = []; // stores all the map-extent elements in the layer + layer._properties._templateVars = []; // stores all template variables coming from all extents + for (let j = 0; j < extents.length; j++) { if ( - this.status === 400 || - this.status === 404 || - this.status === 500 || - this.status === 406 + extents[j].querySelector( + 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' + ) ) { - layer.error = true; - layer.fire('extentload', layer, true); - xhr.abort(); + extents[j]._templateVars = _initTemplateVars.call( + layer, + extents[j], + mapml.querySelector('map-meta[name=extent]'), + layer._properties.projection, + mapml, + base, + projectionMatch + ); + extents[j].extentAnatomy = createLayerControlExtentHTML.call( + layer, + extents[j] + ); + layer._properties._mapExtents.push(extents[j]); + // get rid of layer._properties._templateVars, TBD. + layer._properties._templateVars = + layer._properties._templateVars.concat(extents[j]._templateVars); } } - }; - xhr.onload = fCallback; - xhr.onerror = function () { - layer.error = true; - layer.fire('extentload', layer, true); - }; - xhr.open('GET', url); - xhr.setRequestHeader('Accept', M.mime); - xhr.overrideMimeType('text/xml'); - xhr.send(); - } - function transcribe(element) { - var select = document.createElement('select'); - var elementAttrNames = element.getAttributeNames(); - - for (let i = 0; i < elementAttrNames.length; i++) { - select.setAttribute( - elementAttrNames[i], - element.getAttribute(elementAttrNames[i]) - ); } - - var options = element.children; - - for (let i = 0; i < options.length; i++) { - var option = document.createElement('option'); - var optionAttrNames = options[i].getAttributeNames(); - - for (let j = 0; j < optionAttrNames.length; j++) { - option.setAttribute( - optionAttrNames[j], - options[i].getAttribute(optionAttrNames[j]) - ); + function createLayerControlExtentHTML(mapExtent) { + var extent = L.DomUtil.create('fieldset', 'mapml-layer-extent'), + extentProperties = L.DomUtil.create( + 'div', + 'mapml-layer-item-properties', + extent + ), + extentSettings = L.DomUtil.create( + 'div', + 'mapml-layer-item-settings', + extent + ), + extentLabel = L.DomUtil.create( + 'label', + 'mapml-layer-item-toggle', + extentProperties + ), + input = L.DomUtil.create('input'), + svgExtentControlIcon = L.SVG.create('svg'), + extentControlPath1 = L.SVG.create('path'), + extentControlPath2 = L.SVG.create('path'), + extentNameIcon = L.DomUtil.create('span'), + extentItemControls = L.DomUtil.create( + 'div', + 'mapml-layer-item-controls', + extentProperties + ), + opacityControl = L.DomUtil.create( + 'details', + 'mapml-layer-item-opacity', + extentSettings + ), + extentOpacitySummary = L.DomUtil.create( + 'summary', + '', + opacityControl + ), + mapEl = this._layerEl.parentNode, + layerEl = this._layerEl, + opacity = L.DomUtil.create('input', '', opacityControl); + extentSettings.hidden = true; + extent.setAttribute('aria-grabbed', 'false'); + if (!mapExtent.hasAttribute('label')) { + // if a label attribute is not present, set it to hidden in layer control + extent.setAttribute('hidden', ''); + mapExtent.hidden = true; } - option.innerHTML = options[i].innerHTML; - select.appendChild(option); - } - return select; - } - - function _initTemplateVars( - serverExtent, - metaExtent, - projection, - mapml, - base, - projectionMatch - ) { - var templateVars = []; - // set up the URL template and associated inputs (which yield variable values when processed) - var tlist = serverExtent.querySelectorAll( - 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' - ), - varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), - zoomInput = serverExtent.querySelector('map-input[type="zoom" i]'), - includesZoom = false, - extentFallback = {}; - - extentFallback.zoom = 0; - if (metaExtent) { - let content = M._metaContentToObject( - metaExtent.getAttribute('content') - ), - cs; + // append the svg paths + svgExtentControlIcon.setAttribute('viewBox', '0 0 24 24'); + svgExtentControlIcon.setAttribute('height', '22'); + svgExtentControlIcon.setAttribute('width', '22'); + extentControlPath1.setAttribute('d', 'M0 0h24v24H0z'); + extentControlPath1.setAttribute('fill', 'none'); + extentControlPath2.setAttribute( + 'd', + 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' + ); + svgExtentControlIcon.appendChild(extentControlPath1); + svgExtentControlIcon.appendChild(extentControlPath2); - extentFallback.zoom = content.zoom || extentFallback.zoom; + let removeExtentButton = L.DomUtil.create( + 'button', + 'mapml-layer-item-remove-control', + extentItemControls + ); + removeExtentButton.type = 'button'; + removeExtentButton.title = 'Remove Sub Layer'; + removeExtentButton.innerHTML = + ""; + removeExtentButton.classList.add('mapml-button'); + L.DomEvent.on(removeExtentButton, 'click', L.DomEvent.stop); + L.DomEvent.on( + removeExtentButton, + 'click', + (e) => { + let allRemoved = true; + e.target.checked = false; + mapExtent.removed = true; + mapExtent.checked = false; + if (this._layerEl.checked) this._changeExtent(e, mapExtent); + mapExtent.extentAnatomy.parentNode.removeChild( + mapExtent.extentAnatomy + ); + for (let j = 0; j < this._properties._mapExtents.length; j++) { + if (!this._properties._mapExtents[j].removed) allRemoved = false; + } + if (allRemoved) + this._layerItemSettingsHTML.removeChild( + this._propertiesGroupAnatomy + ); + }, + this + ); - let metaKeys = Object.keys(content); - for (let i = 0; i < metaKeys.length; i++) { - if (!metaKeys[i].includes('zoom')) { - cs = M.axisToCS(metaKeys[i].split('-')[2]); - break; - } - } - let axes = M.csToAxes(cs); - extentFallback.bounds = M.boundsToPCRSBounds( - L.bounds( - L.point( - +content[`top-left-${axes[0]}`], - +content[`top-left-${axes[1]}`] - ), - L.point( - +content[`bottom-right-${axes[0]}`], - +content[`bottom-right-${axes[1]}`] - ) - ), - extentFallback.zoom, - projection, - cs + let extentsettingsButton = L.DomUtil.create( + 'button', + 'mapml-layer-item-settings-control', + extentItemControls + ); + extentsettingsButton.type = 'button'; + extentsettingsButton.title = 'Extent Settings'; + extentsettingsButton.setAttribute('aria-expanded', false); + extentsettingsButton.classList.add('mapml-button'); + L.DomEvent.on( + extentsettingsButton, + 'click', + (e) => { + if (extentSettings.hidden === true) { + extentsettingsButton.setAttribute('aria-expanded', true); + extentSettings.hidden = false; + } else { + extentsettingsButton.setAttribute('aria-expanded', false); + extentSettings.hidden = true; + } + }, + this ); - } else { - extentFallback.bounds = M[projection].options.crs.pcrs.bounds; - } - for (var i = 0; i < tlist.length; i++) { - var t = tlist[i], - template = t.getAttribute('tref'); - t.zoomInput = zoomInput; - if (!template) { - template = BLANK_TT_TREF; - let blankInputs = mapml.querySelectorAll('map-input'); - for (let i of blankInputs) { - template += `{${i.getAttribute('name')}}`; - } - } + extentNameIcon.setAttribute('aria-hidden', true); + extentLabel.appendChild(input); + extentsettingsButton.appendChild(extentNameIcon); + extentNameIcon.appendChild(svgExtentControlIcon); + extentOpacitySummary.innerText = 'Opacity'; + extentOpacitySummary.id = + 'mapml-layer-item-opacity-' + L.stamp(extentOpacitySummary); + opacity.setAttribute('type', 'range'); + opacity.setAttribute('min', '0'); + opacity.setAttribute('max', '1.0'); + opacity.setAttribute('step', '0.1'); + opacity.setAttribute( + 'aria-labelledby', + 'mapml-layer-item-opacity-' + L.stamp(extentOpacitySummary) + ); + let opacityValue = mapExtent.hasAttribute('opacity') + ? mapExtent.getAttribute('opacity') + : '1.0'; + mapExtent._templateVars.opacity = opacityValue; + opacity.setAttribute('value', opacityValue); + opacity.value = opacityValue; + L.DomEvent.on(opacity, 'change', this._changeExtentOpacity, mapExtent); + + var extentItemNameSpan = L.DomUtil.create( + 'span', + 'mapml-layer-item-name', + extentLabel + ); + input.defaultChecked = mapExtent ? true : false; + mapExtent.checked = input.defaultChecked; + input.type = 'checkbox'; + extentItemNameSpan.innerHTML = mapExtent.getAttribute('label'); + L.DomEvent.on(input, 'change', (e) => { + this._changeExtent(e, mapExtent); + }); + extentItemNameSpan.id = + 'mapml-extent-item-name-{' + L.stamp(extentItemNameSpan) + '}'; + extent.setAttribute('aria-labelledby', extentItemNameSpan.id); + extentItemNameSpan.extent = mapExtent; - var v, - title = t.hasAttribute('title') - ? t.getAttribute('title') - : 'Query this layer', - vcount = template.match(varNamesRe), - trel = - !t.hasAttribute('rel') || - t.getAttribute('rel').toLowerCase() === 'tile' - ? 'tile' - : t.getAttribute('rel').toLowerCase(), - ttype = !t.hasAttribute('type') - ? 'image/*' - : t.getAttribute('type').toLowerCase(), - inputs = [], - tms = t && t.hasAttribute('tms'); - var zoomBounds = mapml.querySelector('map-meta[name=zoom]') - ? M._metaContentToObject( - mapml.querySelector('map-meta[name=zoom]').getAttribute('content') - ) - : undefined; - while ((v = varNamesRe.exec(template)) !== null) { - var varName = v[1], - inp = serverExtent.querySelector( - 'map-input[name=' + varName + '],map-select[name=' + varName + ']' - ); - if (inp) { - if ( - inp.hasAttribute('type') && - inp.getAttribute('type') === 'location' && - (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && - inp.hasAttribute('axis') && - !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) - ) { + extent.ontouchstart = extent.onmousedown = (downEvent) => { + if ( + (downEvent.target.parentElement.tagName.toLowerCase() === 'label' && + downEvent.target.tagName.toLowerCase() !== 'input') || + downEvent.target.tagName.toLowerCase() === 'label' + ) { + downEvent.stopPropagation(); + downEvent = + downEvent instanceof TouchEvent + ? downEvent.touches[0] + : downEvent; + + let control = extent, + controls = extent.parentNode, + moving = false, + yPos = downEvent.clientY; + + document.body.ontouchmove = document.body.onmousemove = ( + moveEvent + ) => { + moveEvent.preventDefault(); + moveEvent = + moveEvent instanceof TouchEvent + ? moveEvent.touches[0] + : moveEvent; + + // Fixes flickering by only moving element when there is enough space + let offset = moveEvent.clientY - yPos; + moving = Math.abs(offset) > 5 || moving; if ( - zoomInput && - template.includes(`{${zoomInput.getAttribute('name')}}`) + (controls && !moving) || + (controls && controls.childElementCount <= 1) || + controls.getBoundingClientRect().top > + control.getBoundingClientRect().bottom || + controls.getBoundingClientRect().bottom < + control.getBoundingClientRect().top ) { - zoomInput.setAttribute('value', extentFallback.zoom); + return; } - let axis = inp.getAttribute('axis'), - axisBounds = M.convertPCRSBounds( - extentFallback.bounds, - extentFallback.zoom, - projection, - M.axisToCS(axis) - ); - inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); - inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); - } - inputs.push(inp); - includesZoom = - includesZoom || - (inp.hasAttribute('type') && - inp.getAttribute('type').toLowerCase() === 'zoom'); - if (inp.tagName.toLowerCase() === 'map-select') { - // use a throwaway div to parse the input from MapML into HTML - var div = document.createElement('div'); - div.insertAdjacentHTML('afterbegin', inp.outerHTML); - // parse - inp.htmlselect = div.querySelector('map-select'); - inp.htmlselect = transcribe(inp.htmlselect); - - // this goes into the layer control, so add a listener - L.DomEvent.on(inp.htmlselect, 'change', layer.redraw, layer); - if (!layer._userInputs) { - layer._userInputs = []; + controls.classList.add('mapml-draggable'); + control.style.transform = 'translateY(' + offset + 'px)'; + control.style.pointerEvents = 'none'; + + let x = moveEvent.clientX, + y = moveEvent.clientY, + root = + mapEl.tagName === 'MAPML-VIEWER' + ? mapEl.shadowRoot + : mapEl.querySelector('.mapml-web-map').shadowRoot, + elementAt = root.elementFromPoint(x, y), + swapControl = + !elementAt || !elementAt.closest('fieldset') + ? control + : elementAt.closest('fieldset'); + + swapControl = + Math.abs(offset) <= swapControl.offsetHeight + ? control + : swapControl; + + control.setAttribute('aria-grabbed', 'true'); + control.setAttribute('aria-dropeffect', 'move'); + if (swapControl && controls === swapControl.parentNode) { + swapControl = + swapControl !== control.nextSibling + ? swapControl + : swapControl.nextSibling; + if (control !== swapControl) { + yPos = moveEvent.clientY; + control.style.transform = null; + } + controls.insertBefore(control, swapControl); } - layer._userInputs.push(inp.htmlselect); - } - // TODO: if this is an input@type=location - // get the TCRS min,max attribute values at the identified zoom level - // save this information as properties of the serverExtent, - // perhaps as a bounds object so that it can be easily used - // later by the layer control to determine when to enable - // disable the layer for drawing. - } else { - console.log( - 'input with name=' + - varName + - ' not found for template variable of same name' - ); - // no match found, template won't be used - break; - } - } - if ( - (template && vcount.length === inputs.length) || - template === BLANK_TT_TREF - ) { - if (trel === 'query') { - layer.queryable = true; - } - if (!includesZoom && zoomInput) { - inputs.push(zoomInput); + }; + + document.body.ontouchend = document.body.onmouseup = () => { + control.setAttribute('aria-grabbed', 'false'); + control.removeAttribute('aria-dropeffect'); + control.style.pointerEvents = null; + control.style.transform = null; + let controlsElems = controls.children, + zIndex = 0; + for (let c of controlsElems) { + let extentEl = c.querySelector('span').extent; + + extentEl.setAttribute('data-moving', ''); + layerEl.insertAdjacentElement('beforeend', extentEl); + extentEl.removeAttribute('data-moving'); + + extentEl.extentZIndex = zIndex; + extentEl.templatedLayer.setZIndex(zIndex); + zIndex++; + } + controls.classList.remove('mapml-draggable'); + document.body.ontouchmove = + document.body.onmousemove = + document.body.ontouchend = + document.body.onmouseup = + null; + }; } - let step = zoomInput ? zoomInput.getAttribute('step') : 1; - if (!step || step === '0' || isNaN(step)) step = 1; - // template has a matching input for every variable reference {varref} - templateVars.push({ - template: decodeURI(new URL(template, base)), - linkEl: t, - title: title, - rel: trel, - type: ttype, - values: inputs, - zoomBounds: zoomBounds, - extentPCRSFallback: { bounds: extentFallback.bounds }, - projectionMatch: projectionMatch, - projection: - serverExtent.getAttribute('units') || FALLBACK_PROJECTION, - tms: tms, - step: step - }); - } + }; + return extent; } - return templateVars; - } - - function _processInitialExtent(content) { - //TODO: include inline extents - var mapml = this.responseXML || content; - if (mapml.querySelector && mapml.querySelector('map-feature')) - layer._content = mapml; - if (!this.responseXML && this.responseText) - mapml = new DOMParser().parseFromString(this.responseText, 'text/xml'); - - // if everything is ok, continue with the processing - if ( - this.readyState === this.DONE && - mapml.querySelector && - !mapml.querySelector('parsererror') + function _initTemplateVars( + serverExtent, + metaExtent, + projection, + mapml, + base, + projectionMatch ) { - // Get layer's title/label - if (mapml.querySelector('map-title')) { - layer._title = mapml.querySelector('map-title').textContent.trim(); - layer._titleIsReadOnly = true; - } else if (mapml instanceof Element && mapml.hasAttribute('label')) { - layer._title = mapml.getAttribute('label').trim(); - } + var templateVars = []; + // set up the URL template and associated inputs (which yield variable values when processed) + var tlist = serverExtent.querySelectorAll( + 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' + ), + varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), + zoomInput = serverExtent.querySelector('map-input[type="zoom" i]'), + includesZoom = false, + extentFallback = {}; + + extentFallback.zoom = 0; + if (metaExtent) { + let content = M._metaContentToObject( + metaExtent.getAttribute('content') + ), + cs; - var serverExtent = mapml.querySelectorAll('map-extent'), - projection, - projectionMatch, - serverMeta; + extentFallback.zoom = content.zoom || extentFallback.zoom; - if (!serverExtent.length) { - serverMeta = mapml.querySelector('map-meta[name=projection]'); + let metaKeys = Object.keys(content); + for (let i = 0; i < metaKeys.length; i++) { + if (!metaKeys[i].includes('zoom')) { + cs = M.axisToCS(metaKeys[i].split('-')[2]); + break; + } + } + let axes = M.csToAxes(cs); + extentFallback.bounds = M.boundsToPCRSBounds( + L.bounds( + L.point( + +content[`top-left-${axes[0]}`], + +content[`top-left-${axes[1]}`] + ), + L.point( + +content[`bottom-right-${axes[0]}`], + +content[`bottom-right-${axes[1]}`] + ) + ), + extentFallback.zoom, + projection, + cs + ); + } else { + // for custom projections, M[projection] may not be loaded, so uses M['OSMTILE'] as backup, this code will need to get rerun once projection is changed and M[projection] is available + // TODO: This is a temporary fix, _initTemplateVars (or processinitialextent) should not be called when projection of the layer and map do not match, this should be called/reinitialized once the layer projection matches with the map projection + let fallbackProjection = M[projection] || M.OSMTILE; + extentFallback.bounds = fallbackProjection.options.crs.pcrs.bounds; } - // check whether all map-extent elements have the same units - if (serverExtent.length >= 1) { - for (let i = 0; i < serverExtent.length; i++) { - if ( - serverExtent[i].tagName.toLowerCase() === 'map-extent' && - serverExtent[i].hasAttribute('units') - ) { - projection = serverExtent[i].getAttribute('units'); + for (var i = 0; i < tlist.length; i++) { + var t = tlist[i], + template = t.getAttribute('tref'); + t.zoomInput = zoomInput; + if (!template) { + template = BLANK_TT_TREF; + let blankInputs = mapml.querySelectorAll('map-input'); + for (let i of blankInputs) { + template += `{${i.getAttribute('name')}}`; } - projectionMatch = - projection && projection === layer.options.mapprojection; - if (!projectionMatch) { + } + + var v, + title = t.hasAttribute('title') + ? t.getAttribute('title') + : 'Query this layer', + vcount = template.match(varNamesRe), + trel = + !t.hasAttribute('rel') || + t.getAttribute('rel').toLowerCase() === 'tile' + ? 'tile' + : t.getAttribute('rel').toLowerCase(), + ttype = !t.hasAttribute('type') + ? 'image/*' + : t.getAttribute('type').toLowerCase(), + inputs = [], + tms = t && t.hasAttribute('tms'); + var zoomBounds = mapml.querySelector('map-meta[name=zoom]') + ? M._metaContentToObject( + mapml + .querySelector('map-meta[name=zoom]') + .getAttribute('content') + ) + : undefined; + while ((v = varNamesRe.exec(template)) !== null) { + var varName = v[1], + inp = serverExtent.querySelector( + 'map-input[name=' + + varName + + '],map-select[name=' + + varName + + ']' + ); + if (inp) { + if ( + inp.hasAttribute('type') && + inp.getAttribute('type') === 'location' && + (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && + inp.hasAttribute('axis') && + !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) + ) { + if ( + zoomInput && + template.includes(`{${zoomInput.getAttribute('name')}}`) + ) { + zoomInput.setAttribute('value', extentFallback.zoom); + } + let axis = inp.getAttribute('axis'), + axisBounds = M.convertPCRSBounds( + extentFallback.bounds, + extentFallback.zoom, + projection, + M.axisToCS(axis) + ); + inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); + inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); + } + + inputs.push(inp); + includesZoom = + includesZoom || + (inp.hasAttribute('type') && + inp.getAttribute('type').toLowerCase() === 'zoom'); + if (inp.tagName.toLowerCase() === 'map-select') { + // use a throwaway div to parse the input from MapML into HTML + var div = document.createElement('div'); + div.insertAdjacentHTML('afterbegin', inp.outerHTML); + // parse + inp.htmlselect = div.querySelector('map-select'); + inp.htmlselect = transcribe(inp.htmlselect); + + // this goes into the layer control, so add a listener + L.DomEvent.on(inp.htmlselect, 'change', layer.redraw, layer); + if (!layer._userInputs) { + layer._userInputs = []; + } + layer._userInputs.push(inp.htmlselect); + } + // TODO: if this is an input@type=location + // get the TCRS min,max attribute values at the identified zoom level + // save this information as properties of the serverExtent, + // perhaps as a bounds object so that it can be easily used + // later by the layer control to determine when to enable + // disable the layer for drawing. + } else { + console.log( + 'input with name=' + + varName + + ' not found for template variable of same name' + ); + // no match found, template won't be used break; } } - } else if (serverMeta) { if ( - serverMeta.tagName.toLowerCase() === 'map-meta' && - serverMeta.hasAttribute('content') + (template && vcount.length === inputs.length) || + template === BLANK_TT_TREF ) { - projection = M._metaContentToObject( - serverMeta.getAttribute('content') - ).content; - projectionMatch = - projection && projection === layer.options.mapprojection; + if (trel === 'query') { + layer.queryable = true; + } + if (!includesZoom && zoomInput) { + inputs.push(zoomInput); + } + let step = zoomInput ? zoomInput.getAttribute('step') : 1; + if (!step || step === '0' || isNaN(step)) step = 1; + // template has a matching input for every variable reference {varref} + templateVars.push({ + template: decodeURI(new URL(template, base)), + linkEl: t, + title: title, + rel: trel, + type: ttype, + values: inputs, + zoomBounds: zoomBounds, + extentPCRSFallback: { bounds: extentFallback.bounds }, + projectionMatch: projectionMatch, + projection: + serverExtent.getAttribute('units') || FALLBACK_PROJECTION, + tms: tms, + step: step + }); } - } else { - // default projection set to parent projection when no map-meta projection element present - projection = layer.options.mapprojection; - projectionMatch = true; - serverMeta = projection; - console.log( - `A projection was not assigned to the '${layer._title}' Layer. Please specify a projection for that layer using a map-meta element. See more here - https://maps4html.org/web-map-doc/docs/elements/meta/` + } + return templateVars; + } + function transcribe(element) { + var select = document.createElement('select'); + var elementAttrNames = element.getAttributeNames(); + + for (let i = 0; i < elementAttrNames.length; i++) { + select.setAttribute( + elementAttrNames[i], + element.getAttribute(elementAttrNames[i]) ); - // TODO: Add a more obvious warning. } - var metaExtent = mapml.querySelector('map-meta[name=extent]'), - selectedAlternate = - !projectionMatch && - mapml.querySelector( - 'map-head map-link[rel=alternate][projection=' + - layer.options.mapprojection + - ']' - ), - base = new URL( - mapml.querySelector('map-base') - ? mapml.querySelector('map-base').getAttribute('href') - : mapml.baseURI || this.responseURL, - this.responseURL - ).href; + var options = element.children; - if ( - !projectionMatch && - selectedAlternate && - selectedAlternate.hasAttribute('href') - ) { - layer.fire( - 'changeprojection', - { - href: new URL(selectedAlternate.getAttribute('href'), base).href - }, - false - ); - return; - } else if ( - !projectionMatch && - layer._map && - layer._map.options.mapEl.querySelectorAll('layer-').length === 1 - ) { - layer._map.options.mapEl.projection = projection; - return; - } else if (!serverMeta) { - layer._extent = {}; - if (projectionMatch) { - layer._extent.crs = M[projection]; - } - layer._extent._mapExtents = []; // stores all the map-extent elements in the layer - layer._extent._templateVars = []; // stores all template variables coming from all extents - for (let j = 0; j < serverExtent.length; j++) { - if ( - serverExtent[j].querySelector( - 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' - ) && - serverExtent[j].hasAttribute('units') - ) { - layer._extent._mapExtents.push(serverExtent[j]); - projectionMatch = projectionMatch || selectedAlternate; - let templateVars = _initTemplateVars.call( - layer, - serverExtent[j], - metaExtent, - projection, - mapml, - base, - projectionMatch - ); - layer._extent._mapExtents[j]._templateVars = templateVars; - layer._extent._templateVars = - layer._extent._templateVars.concat(templateVars); - } - } - } else { - if (typeof serverMeta === 'string') { - // when map-meta projection not present for layer - layer._extent = { serverMeta }; - } else { - // when map-meta projection present for layer - layer._extent = serverMeta; + for (let i = 0; i < options.length; i++) { + var option = document.createElement('option'); + var optionAttrNames = options[i].getAttributeNames(); + + for (let j = 0; j < optionAttrNames.length; j++) { + option.setAttribute( + optionAttrNames[j], + options[i].getAttribute(optionAttrNames[j]) + ); } - } - layer._parseLicenseAndLegend(mapml, layer, projection); + option.innerHTML = options[i].innerHTML; + select.appendChild(option); + } + return select; + } + function setZoomInOrOutLinks() { var zoomin = mapml.querySelector('map-link[rel=zoomin]'), zoomout = mapml.querySelector('map-link[rel=zoomout]'); - delete layer._extent.zoomin; - delete layer._extent.zoomout; if (zoomin) { - layer._extent.zoomin = new URL( + layer._properties.zoomin = new URL( zoomin.getAttribute('href'), base ).href; } if (zoomout) { - layer._extent.zoomout = new URL( + layer._properties.zoomout = new URL( zoomout.getAttribute('href'), base ).href; } - if (layer._extent._mapExtents) { - for (let i = 0; i < layer._extent._mapExtents.length; i++) { - if (layer._extent._mapExtents[i].templatedLayer) { - layer._extent._mapExtents[i].templatedLayer.reset( - layer._extent._mapExtents[i]._templateVars, - layer._extent._mapExtents[i].extentZIndex - ); + } + function processFeatures() { + let native = M.getNativeVariables(layer._content); + layer._mapmlvectors = M.featureLayer(null, { + // pass the vector layer a renderer of its own, otherwise leaflet + // puts everything into the overlayPane + renderer: M.featureRenderer(), + // pass the vector layer the container for the parent into which + // it will append its own container for rendering into + pane: layer._container, + opacity: layer.options.opacity, + projection: layer._properties.projection, + // by NOT passing options.extent, we are asking the FeatureLayer + // to dynamically update its .layerBounds property as features are + // added or removed from it + native: native, + // each owned child layer gets a reference to the root layer + _leafletLayer: layer, + static: true, + onEachFeature: function (properties, geometry) { + // need to parse as HTML to preserve semantics and styles + if (properties) { + var c = document.createElement('div'); + c.classList.add('mapml-popup-content'); + c.insertAdjacentHTML('afterbegin', properties.innerHTML); + geometry.bindPopup(c, { autoClose: false, minWidth: 165 }); } } - } + }); + } + function processTiles() { if (mapml.querySelector('map-tile')) { var tiles = document.createElement('map-tiles'), zoom = @@ -1580,18 +1541,18 @@ export var MapMLLayer = L.Layer.extend({ tiles.appendChild(document.importNode(newTiles[nt], true)); } layer._mapmlTileContainer.appendChild(tiles); + layer._staticTileLayer = M.staticTileLayer({ + pane: layer._container, + _leafletLayer: layer, + projection: layer._properties.projection, + className: 'mapml-static-tile-layer', + tileContainer: layer._mapmlTileContainer, + maxZoomBound: layer._properties.crs.options.resolutions.length - 1, + tileSize: layer._properties.crs.options.crs.tile.bounds.max.x + }); } - M._parseStylesheetAsHTML(mapml, base, layer._container); - - // add multiple extents - if (layer._extent._mapExtents) { - for (let j = 0; j < layer._extent._mapExtents.length; j++) { - var labelName = layer._extent._mapExtents[j].getAttribute('label'); - var extentElement = layer.getLayerExtentHTML(labelName, j); - layer._extent._mapExtents[j].extentAnatomy = extentElement; - } - } - + } + function getAlternateStyles() { var styleLinks = mapml.querySelectorAll( 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' ); @@ -1600,11 +1561,15 @@ export var MapMLLayer = L.Layer.extend({ stylesControlSummary = document.createElement('summary'); stylesControlSummary.innerText = 'Style'; stylesControl.appendChild(stylesControlSummary); + var changeStyle = function (e) { - layer.fire( - 'changestyle', - { src: e.target.getAttribute('data-href') }, - false + L.DomEvent.stop(e); + layer._layerEl.dispatchEvent( + new CustomEvent('changestyle', { + detail: { + src: e.target.getAttribute('data-href') + } + }) ); }; @@ -1618,7 +1583,7 @@ export var MapMLLayer = L.Layer.extend({ 'id', 'rad-' + L.stamp(styleOptionInput) ); - styleOptionInput.setAttribute('name', 'styles-' + this._title); + styleOptionInput.setAttribute('name', 'styles-' + layer._title); styleOptionInput.setAttribute( 'value', styleLinks[j].getAttribute('title') @@ -1648,41 +1613,27 @@ export var MapMLLayer = L.Layer.extend({ ); L.DomEvent.on(styleOptionInput, 'click', changeStyle, layer); } - layer._styles = stylesControl; + return stylesControl; } - - if (layer._map) { - layer._validateExtent(); - // if the layer is checked in the layer control, force the addition - // of the attribution just received - if (layer._map.hasLayer(layer)) { - layer._map.attributionControl.addAttribution( - layer.getAttribution() - ); - } - //layer._map.fire('moveend', layer); - } - } else { - layer.error = true; - } - if (this.responseXML) { - _attachToLayer.call(layer); } - layer.fire('extentload', layer, false); - // update controls if needed based on mapml-viewer controls/controlslist attribute - if (layer._layerEl.parentElement) { - // if layer does not have a parent Element, do not need to set Controls - layer._layerEl.parentElement._toggleControls(); + function setLayerTitle() { + if (mapml.querySelector('map-title')) { + layer._title = mapml.querySelector('map-title').textContent.trim(); + layer._titleIsReadOnly = true; + } else if (mapml instanceof Element && mapml.hasAttribute('label')) { + layer._title = mapml.getAttribute('label').trim(); + } + // _mapmlLayerItem is set to the root element representing this layer + // in the layer control, iff the layer is not 'hidden' + layer._createLayerControlHTML(); } - layer._layerEl.dispatchEvent( - new CustomEvent('extentload', { detail: layer, bubbles: true }) - ); - } - - function _attachToLayer() { - let mapml = xhr.responseXML, - shadowRoot = this._layerEl.shadowRoot; - if (mapml) { + function copyRemoteContentToShadowRoot() { + // only run when content is loaded from network, puts features etc + // into layer shadow root + if (local) { + return; + } + let shadowRoot = layer._layerEl.shadowRoot; let elements = mapml.children[0].children[1].children; if (elements) { let baseURL = mapml.children[0].children[0] @@ -1713,16 +1664,48 @@ export var MapMLLayer = L.Layer.extend({ } } } + function parseLicenseAndLegend() { + var licenseLink = mapml.querySelector('map-link[rel=license]'), + licenseTitle, + licenseUrl, + attText; + if (licenseLink) { + licenseTitle = licenseLink.getAttribute('title'); + licenseUrl = licenseLink.getAttribute('href'); + attText = + '' + + licenseTitle + + ''; + } + L.setOptions(layer, { attribution: attText }); + var legendLink = mapml.querySelector('map-link[rel=legend]'); + if (legendLink) { + layer._legendUrl = legendLink.getAttribute('href'); + } + if (layer._map) { + // if the layer is checked in the layer control, force the addition + // of the attribution just received + if (layer._map.hasLayer(layer)) { + layer._map.attributionControl.addAttribution( + layer.getAttribution() + ); + } + } + } } }, _validateExtent: function () { // TODO: change so that the _extent bounds are set based on inputs - if (!this._extent || !this._map) { + if (!this._properties || !this._map) { return; } - var serverExtent = this._extent._mapExtents - ? this._extent._mapExtents - : [this._extent], + var serverExtent = this._properties._mapExtents + ? this._properties._mapExtents + : [this._properties], lp; // loop through the map-extent elements and assign each one its crs @@ -1743,70 +1726,25 @@ export var MapMLLayer = L.Layer.extend({ ? serverExtent[i].getAttribute('units') : null; if (lp && M[lp]) { - if (this._extent._mapExtents) this._extent._mapExtents[i].crs = M[lp]; - else this._extent.crs = M[lp]; + if (this._properties._mapExtents) + this._properties._mapExtents[i].crs = M[lp]; + else this._properties.crs = M[lp]; } else { - if (this._extent._mapExtents) - this._extent._mapExtents[i].crs = M.OSMTILE; - else this._extent.crs = M.OSMTILE; + if (this._properties._mapExtents) + this._properties._mapExtents[i].crs = M.OSMTILE; + else this._properties.crs = M.OSMTILE; } } }, - // a layer must share a projection with the map so that all the layers can - // be overlayed in one coordinate space. WGS84 is a 'wildcard', sort of. + // new getProjection, maybe simpler, but doesn't work... getProjection: function () { - if (!this._extent) { + if (!this._properties) { return; } - let extent = this._extent._mapExtents - ? this._extent._mapExtents[0] - : this._extent; // the projections for each extent eould be the same (as) validated in _validProjection, so can use mapExtents[0] - if (extent.serverMeta) return extent.serverMeta; - switch (extent.tagName.toUpperCase()) { - case 'MAP-EXTENT': - if (extent.hasAttribute('units')) - return extent.getAttribute('units').toUpperCase(); - break; - case 'MAP-INPUT': - if (extent.hasAttribute('value')) - return extent.getAttribute('value').toUpperCase(); - break; - case 'MAP-META': - if (extent.hasAttribute('content')) - return M._metaContentToObject( - extent.getAttribute('content') - ).content.toUpperCase(); - break; - default: - return FALLBACK_PROJECTION; - } - return FALLBACK_PROJECTION; - }, - _parseLicenseAndLegend: function (xml, layer) { - var licenseLink = xml.querySelector('map-link[rel=license]'), - licenseTitle, - licenseUrl, - attText; - if (licenseLink) { - licenseTitle = licenseLink.getAttribute('title'); - licenseUrl = licenseLink.getAttribute('href'); - attText = - '' + - licenseTitle + - ''; - } - L.setOptions(layer, { attribution: attText }); - var legendLink = xml.querySelector('map-link[rel=legend]'); - if (legendLink) { - layer._legendUrl = legendLink.getAttribute('href'); - } + return this._properties.projection; }, getQueryTemplates: function (pcrsClick) { - if (this._extent && this._extent._queries) { + if (this._properties && this._properties._queries) { var templates = []; // only return queries that are in bounds if ( @@ -1820,17 +1758,17 @@ export var MapMLLayer = L.Layer.extend({ for (let i = 0; i < layerAndExtents.length; i++) { if ( layerAndExtents[i].extent || - this._extent._mapExtents.length === 1 + this._properties._mapExtents.length === 1 ) { // the layer won't have an .extent property, this is kind of a hack let extent = - layerAndExtents[i].extent || this._extent._mapExtents[0]; + layerAndExtents[i].extent || this._properties._mapExtents[0]; for (let j = 0; j < extent._templateVars.length; j++) { if (extent.checked) { let template = extent._templateVars[j]; - // for each template in the extent, see if it corresponds to one in the this._extent._queries array - for (let k = 0; k < this._extent._queries.length; k++) { - let queryTemplate = this._extent._queries[k]; + // for each template in the extent, see if it corresponds to one in the this._properties._queries array + for (let k = 0; k < this._properties._queries.length; k++) { + let queryTemplate = this._properties._queries[k]; if ( template === queryTemplate && queryTemplate.extentBounds.contains(pcrsClick) @@ -2061,26 +1999,42 @@ export var MapMLLayer = L.Layer.extend({ content.querySelector('a.mapml-zoom-link').remove(); } if (!featureEl.querySelector('map-geometry')) return; - let tL = featureEl.extent.topLeft.gcrs, - bR = featureEl.extent.bottomRight.gcrs, - center = L.latLngBounds( - L.latLng(tL.horizontal, tL.vertical), - L.latLng(bR.horizontal, bR.vertical) - ).getCenter(true); - let zoomLink = document.createElement('a'); - zoomLink.href = `#${featureEl.getMaxZoom()},${center.lng},${center.lat}`; - zoomLink.innerHTML = `${M.options.locale.popupZoom}`; - zoomLink.className = 'mapml-zoom-link'; - zoomLink.onclick = zoomLink.onkeydown = function (e) { - if (!(e instanceof MouseEvent) && e.keyCode !== 13) return; - e.preventDefault(); - featureEl.zoomTo(); - featureEl._map.closePopup(); - }; - content.insertBefore( - zoomLink, - content.querySelector('hr.mapml-popup-divider') - ); + featureEl.whenReady().then(() => { + let tL = featureEl.extent.topLeft.gcrs, + bR = featureEl.extent.bottomRight.gcrs, + center = L.latLngBounds( + L.latLng(tL.horizontal, tL.vertical), + L.latLng(bR.horizontal, bR.vertical) + ).getCenter(true); + let zoomLink = document.createElement('a'); + zoomLink.href = `#${featureEl.getMaxZoom()},${center.lng},${ + center.lat + }`; + zoomLink.innerHTML = `${M.options.locale.popupZoom}`; + zoomLink.className = 'mapml-zoom-link'; + zoomLink.onclick = zoomLink.onkeydown = function (e) { + if (!(e instanceof MouseEvent) && e.keyCode !== 13) return; + e.preventDefault(); + featureEl.zoomTo(); + featureEl._map.closePopup(); + featureEl._map.getContainer().focus(); + }; + // we found that the popupopen event is fired as many times as there + // are layers on the map ( elements / MapMLLayers that is). + // In each case the target layer is always this layer, so we can't + // detect and conditionally add the zoomLink if the target is not this. + // so, like Ahmad, we are taking a 'delete everyting each time' + // approach (see _attachSkipButtons for this approach taken with + // feature navigation buttons); obviously he dealt with this leaflet bug + // this way some time ago, and we can't figure out how to get around it + // apart from this slightly non-optimal method. Revisit sometime! + let link = content.querySelector('.mapml-zoom-link'); + if (link) link.remove(); + content.insertBefore( + zoomLink, + content.querySelector('hr.mapml-popup-divider') + ); + }); } // if popup closes then the focusFeature handler can be removed @@ -2096,7 +2050,7 @@ export var MapMLLayer = L.Layer.extend({ } } }); -export var mapMLLayer = function (url, node, options) { +export var mapMLLayer = function (url, node, mapml, options) { if (!url && !node) return null; - return new MapMLLayer(url, node, options); + return new MapMLLayer(url, node, mapml, options); }; diff --git a/src/mapml/layers/StaticTileLayer.js b/src/mapml/layers/StaticTileLayer.js index 8fc313574..91d66501b 100644 --- a/src/mapml/layers/StaticTileLayer.js +++ b/src/mapml/layers/StaticTileLayer.js @@ -9,18 +9,16 @@ export var StaticTileLayer = L.GridLayer.extend({ this._groups = this._groupTiles( this.options.tileContainer.getElementsByTagName('map-tile') ); - }, - - onAdd: function () { - this._bounds = this._getLayerBounds( - this._groups, - this._map.options.projection - ); //stores meter values of bounds + this._bounds = this._getLayerBounds(this._groups, this.options.projection); //stores meter values of bounds this.layerBounds = this._bounds[Object.keys(this._bounds)[0]]; for (let key of Object.keys(this._bounds)) { this.layerBounds.extend(this._bounds[key].min); this.layerBounds.extend(this._bounds[key].max); } + }, + + onAdd: function (map) { + this._map = map; L.GridLayer.prototype.onAdd.call(this, this._map); this._handleMoveEnd(); }, @@ -116,7 +114,6 @@ export var StaticTileLayer = L.GridLayer.extend({ projection ); } - return layerBounds; }, diff --git a/src/mapml/layers/TemplatedFeaturesLayer.js b/src/mapml/layers/TemplatedFeaturesLayer.js index 2f2e3c3c1..ab4e74268 100644 --- a/src/mapml/layers/TemplatedFeaturesLayer.js +++ b/src/mapml/layers/TemplatedFeaturesLayer.js @@ -35,6 +35,9 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ // pass the vector layer the container for the parent into which // it will append its own container for rendering into pane: container, + // the bounds will be static, fixed, constant for the lifetime of the layer + layerBounds: this.extentBounds, + zoomBounds: this.zoomBounds, opacity: opacity, projection: map.options.projection, static: true, diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index c2ee4619a..6daea770d 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -166,6 +166,8 @@ export var TemplatedTileLayer = L.TileLayer.extend({ let tileFeatures = M.featureLayer(markup, { projection: this._map.options.projection, static: false, + layerBounds: this.extentBounds, + zoomBounds: this.zoomBounds, interactive: false }); diff --git a/src/mapml/options.js b/src/mapml/options.js index 73995b365..008ff476f 100644 --- a/src/mapml/options.js +++ b/src/mapml/options.js @@ -53,6 +53,7 @@ export var Options = { kbdNextFeature: 'Next feature', dfLayer: 'Layer', popupZoom: 'Zoom to here', - dfPastedLayer: 'Pasted layer' + dfPastedLayer: 'Pasted layer', + fIndexNoFeatures: 'No features found' } }; diff --git a/src/mapml/utils/Util.js b/src/mapml/utils/Util.js index e00feb307..455b0a1cc 100644 --- a/src/mapml/utils/Util.js +++ b/src/mapml/utils/Util.js @@ -2,26 +2,20 @@ import { FALLBACK_CS, FALLBACK_PROJECTION } from './Constants'; export var Util = { // _convertAndFormatPCRS returns the converted CRS and formatted pcrsBounds in gcrs, pcrs, tcrs, and tilematrix. Used for setting extent for the map and layer (map.extent, layer.extent). - // _convertAndFormatPCRS: L.Bounds, _map -> {...} - _convertAndFormatPCRS: function (pcrsBounds, map) { - if (!pcrsBounds || !map) return {}; + // _convertAndFormatPCRS: L.Bounds, _map, projection -> {...} + _convertAndFormatPCRS: function (pcrsBounds, crs, projection) { + if (!pcrsBounds || !crs) return {}; let tcrsTopLeft = [], tcrsBottomRight = [], tileMatrixTopLeft = [], tileMatrixBottomRight = [], - tileSize = map.options.crs.options.crs.tile.bounds.max.y; + tileSize = crs.options.crs.tile.bounds.max.y; - for (let i = 0; i < map.options.crs.options.resolutions.length; i++) { - let scale = map.options.crs.scale(i), - minConverted = map.options.crs.transformation.transform( - pcrsBounds.min, - scale - ), - maxConverted = map.options.crs.transformation.transform( - pcrsBounds.max, - scale - ); + for (let i = 0; i < crs.options.resolutions.length; i++) { + let scale = crs.scale(i), + minConverted = crs.transformation.transform(pcrsBounds.min, scale), + maxConverted = crs.transformation.transform(pcrsBounds.max, scale); tcrsTopLeft.push({ horizontal: minConverted.x, @@ -44,8 +38,8 @@ export var Util = { } //converts the gcrs, I believe it can take any number values from -inf to +inf - let unprojectedMin = map.options.crs.unproject(pcrsBounds.min), - unprojectedMax = map.options.crs.unproject(pcrsBounds.max); + let unprojectedMin = crs.unproject(pcrsBounds.min), + unprojectedMax = crs.unproject(pcrsBounds.max); let gcrs = { topLeft: { @@ -71,7 +65,7 @@ export var Util = { }; //formats all extent data - return { + let extent = { topLeft: { tcrs: tcrsTopLeft, tilematrix: tileMatrixTopLeft, @@ -83,9 +77,12 @@ export var Util = { tilematrix: tileMatrixBottomRight, gcrs: gcrs.bottomRight, pcrs: pcrs.bottomRight - }, - projection: map.options.projection + } }; + if (projection) { + extent.projection = projection; + } + return extent; }, // _extractInputBounds extracts and returns Input Bounds from the provided template @@ -503,7 +500,6 @@ export var Util = { if (['/', '.', '#'].includes(link.url[0])) link.target = '_self'; } if (!justPan) { - let newLayer = false; layer = document.createElement('layer-'); layer.setAttribute('src', link.url); layer.setAttribute('checked', ''); @@ -512,44 +508,26 @@ export var Util = { if (link.type === 'text/html') { window.open(link.url); } else { + postTraversalSetup(); map.options.mapEl.appendChild(layer); - newLayer = true; } break; case '_parent': + postTraversalSetup(); for (let l of map.options.mapEl.querySelectorAll('layer-')) if (l._layer !== leafletLayer) map.options.mapEl.removeChild(l); map.options.mapEl.appendChild(layer); map.options.mapEl.removeChild(leafletLayer._layerEl); - newLayer = true; break; case '_top': window.location.href = link.url; break; default: + postTraversalSetup(); opacity = leafletLayer._layerEl.opacity; leafletLayer._layerEl.insertAdjacentElement('beforebegin', layer); map.options.mapEl.removeChild(leafletLayer._layerEl); - newLayer = true; } - if (!link.inPlace && newLayer) - L.DomEvent.on(layer, 'extentload', function focusOnLoad(e) { - if ( - newLayer && - ['_parent', '_self'].includes(link.target) && - layer.parentElement.querySelectorAll('layer-').length === 1 - ) - layer.parentElement.projection = layer._layer.getProjection(); - if (layer.extent) { - if (zoomTo) - layer.parentElement.zoomTo(+zoomTo.lat, +zoomTo.lng, +zoomTo.z); - else layer.zoomTo(); - L.DomEvent.off(layer, 'extentload', focusOnLoad); - } - - if (opacity) layer.opacity = opacity; - map.getContainer().focus(); - }); } else if (zoomTo && !link.inPlace && justPan) { leafletLayer._map.options.mapEl.zoomTo( +zoomTo.lat, @@ -557,7 +535,178 @@ export var Util = { +zoomTo.z ); if (opacity) layer.opacity = opacity; + map.getContainer().focus(); + } + + function postTraversalSetup() { + // when the projection is changed as part of the link traversal process, + // it's necessary to set the map viewer's lat, lon and zoom NOW, so that + // the promises that are created when the viewer's projection is changed + // can use the viewer's lat, lon and zoom properties that were in effect + // before the projection change i.e. in the closure for that code + // see mapml-viewer / map is=web-map projection attributeChangedCallback + // specifically required for use cases like changing projection after + // link traversal, e.g. BC link here https://maps4html.org/experiments/linking/features/ + if (!link.inPlace && zoomTo) updateMapZoomTo(zoomTo); + // the layer is newly created, so have to wait until it's fully init'd + // before setting properties. + layer.whenReady().then(() => { + // TODO refactor _setLayerElExtent so that it's invoked automatically + // by layer.extent getter TBD. + if (!layer.extent) { + layer._layer._setLayerElExtent(); + } + // if the map projection isnt' changed by link traversal, it's necessary + // to perform pan/zoom operations after the layer is ready + if (!link.inPlace && zoomTo) + layer.parentElement.zoomTo(+zoomTo.lat, +zoomTo.lng, +zoomTo.z); + else if (!link.inPlace) layer.zoomTo(); + // not sure if this is necessary + if (opacity) layer.opacity = opacity; + // this is necessary to display the FeatureIndexOverlay, I believe + map.getContainer().focus(); + }); + } + + function updateMapZoomTo(zoomTo) { + // can't use mapEl.zoomTo(...) here, it's too slow! + map.options.mapEl.lat = +zoomTo.lat; + map.options.mapEl.lon = +zoomTo.lng; + map.options.mapEl.zoom = +zoomTo.z; + } + }, + getBounds: function (mapml) { + if (!mapml) return null; + let cs = FALLBACK_CS, + projection = + (mapml.querySelector('map-meta[name=projection]') && + M._metaContentToObject( + mapml + .querySelector('map-meta[name=projection]') + .getAttribute('content') + ).content.toUpperCase()) || + FALLBACK_PROJECTION; + try { + let meta = + mapml.querySelector('map-meta[name=extent]') && + M._metaContentToObject( + mapml.querySelector('map-meta[name=extent]').getAttribute('content') + ); + + let zoom = meta.zoom || 0; + + let metaKeys = Object.keys(meta); + for (let i = 0; i < metaKeys.length; i++) { + if (!metaKeys[i].includes('zoom')) { + cs = M.axisToCS(metaKeys[i].split('-')[2]); + break; + } + } + let axes = M.csToAxes(cs); + return M.boundsToPCRSBounds( + L.bounds( + L.point(+meta[`top-left-${axes[0]}`], +meta[`top-left-${axes[1]}`]), + L.point( + +meta[`bottom-right-${axes[0]}`], + +meta[`bottom-right-${axes[1]}`] + ) + ), + zoom, + projection, + cs + ); + } catch (error) { + //if error then by default set the layer to osm and bounds to the entire map view + return M.boundsToPCRSBounds( + M[projection].options.crs.tilematrix.bounds(0), + 0, + projection, + cs + ); + } + }, + getZoomBounds: function (mapml, nativeZoom) { + if (!mapml) return null; + let nMin = 100, + nMax = 0, + features = mapml.querySelectorAll('map-feature'), + meta, + projection; + for (let i = 0; i < features.length; i++) { + let lZoom = +features[i].getAttribute('zoom'); + if (!features[i].getAttribute('zoom')) lZoom = nativeZoom; + nMax = Math.max(nMax, lZoom); + nMin = Math.min(nMin, lZoom); + } + try { + projection = M._metaContentToObject( + mapml.querySelector('map-meta[name=projection]').getAttribute('content') + ).content; + meta = M._metaContentToObject( + mapml.querySelector('map-meta[name=zoom]').getAttribute('content') + ); + } catch (error) { + return { + minZoom: 0, + maxZoom: + M[projection || FALLBACK_PROJECTION].options.resolutions.length - 1, + minNativeZoom: nMin, + maxNativeZoom: nMax + }; + } + return { + minZoom: +meta.min, + maxZoom: +meta.max, + minNativeZoom: nMin, + maxNativeZoom: nMax + }; + }, + // getNativeVariables: returns an object with the native zoom and CS, + // based on the map-metas that are available within + // the layer or the fallback default values. + // getNativeVariables: mapml-||layer-||null||[map-feature,...] -> {zoom: _, val: _} + // mapml can be a mapml- element, layer- element, null, or an array of map-features + getNativeVariables: function (mapml) { + let nativeZoom, nativeCS; + // when mapml is an array of features provided by the query + if ( + mapml.length && + mapml[0].parentElement.parentElement && + mapml[0].parentElement.parentElement.tagName === 'mapml-' + ) { + let mapmlEl = mapml[0].parentElement.parentElement; + nativeZoom = + (mapmlEl.querySelector && + mapmlEl.querySelector('map-meta[name=zoom]') && + +M._metaContentToObject( + mapmlEl.querySelector('map-meta[name=zoom]').getAttribute('content') + ).value) || + 0; + nativeCS = + (mapmlEl.querySelector && + mapmlEl.querySelector('map-meta[name=cs]') && + M._metaContentToObject( + mapmlEl.querySelector('map-meta[name=cs]').getAttribute('content') + ).content) || + 'GCRS'; + } else { + // when mapml is null or a layer-/mapml- element + nativeZoom = + (mapml.querySelector && + mapml.querySelector('map-meta[name=zoom]') && + +M._metaContentToObject( + mapml.querySelector('map-meta[name=zoom]').getAttribute('content') + ).value) || + 0; + nativeCS = + (mapml.querySelector && + mapml.querySelector('map-meta[name=cs]') && + M._metaContentToObject( + mapml.querySelector('map-meta[name=cs]').getAttribute('content') + ).content) || + 'GCRS'; } + return { zoom: nativeZoom, cs: nativeCS }; }, // _gcrsToTileMatrix returns the [column, row] of the tiles at map center. Used for Announce movement for screen readers @@ -585,7 +734,7 @@ export var Util = { M.options.locale.dfLayer + '" checked="">'; mapEl.insertAdjacentHTML('beforeend', l); - mapEl.lastChild.addEventListener('error', function () { + mapEl.lastElementChild.whenReady().catch(() => { if (mapEl) { // should invoke lifecyle callbacks automatically by removing it from DOM mapEl.removeChild(mapEl.lastChild); diff --git a/src/web-map.js b/src/web-map.js index ff03f80ae..2d1e24056 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -80,19 +80,15 @@ export class WebMap extends HTMLMapElement { : 'OSMTILE'; } set projection(val) { - if (val && M[val]) { - this.setAttribute('projection', val); - if (this._map && this._map.options.projection !== val) { - this._map.options.crs = M[val]; - this._map.options.projection = val; - for (let layer of this.querySelectorAll('layer-')) { - layer.removeAttribute('disabled'); - let reAttach = this.removeChild(layer); - this.appendChild(reAttach); - } - if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); - } else this.dispatchEvent(new CustomEvent('createmap')); - } else throw new Error('Undefined Projection'); + if (val) { + this.whenProjectionDefined(val) + .then(() => { + this.setAttribute('projection', val); + }) + .catch(() => { + throw new Error('Undefined projection:' + val); + }); + } } get zoom() { return this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0; @@ -117,7 +113,11 @@ export class WebMap extends HTMLMapElement { map.getZoom(), map.options.projection ); - let formattedExtent = M._convertAndFormatPCRS(pcrsBounds, map); + let formattedExtent = M._convertAndFormatPCRS( + pcrsBounds, + map.options.crs, + this.projection + ); if (map.getMaxZoom() !== Infinity) { formattedExtent.zoom = { minZoom: map.getMinZoom(), @@ -145,79 +145,68 @@ export class WebMap extends HTMLMapElement { this._traversalCall = false; } connectedCallback() { - this._initShadowRoot(); - - this._controlsList = new DOMTokenList( - this.getAttribute('controlslist'), - this, - 'controlslist', - [ - 'noreload', - 'nofullscreen', - 'nozoom', - 'nolayer', - 'noscale', - 'geolocation' - ] - ); + this.whenProjectionDefined(this.projection) + .then(() => { + this._initShadowRoot(); + + this._controlsList = new DOMTokenList( + this.getAttribute('controlslist'), + this, + 'controlslist', + [ + 'noreload', + 'nofullscreen', + 'nozoom', + 'nolayer', + 'noscale', + 'geolocation' + ] + ); - var s = window.getComputedStyle(this), - wpx = s.width, - hpx = s.height, - w = this.hasAttribute('width') - ? this.getAttribute('width') - : parseInt(wpx.replace('px', '')), - h = this.hasAttribute('height') - ? this.getAttribute('height') - : parseInt(hpx.replace('px', '')); - this._changeWidth(w); - this._changeHeight(h); - - // wait for createmap event before creating leaflet map - // this allows a safeguard for the case where loading a custom TCRS takes - // longer than loading mapml-viewer.js/web-map.js - // the REASON we need a synchronous event listener (see comment below) - // is because the mapml-viewer element has / can have a size of 0 up until after - // something that happens between this point and the event handler executing - // perhaps a browser rendering cycle?? - this.addEventListener('createmap', this._createMap); - - let custom = !['CBMTILE', 'APSTILE', 'OSMTILE', 'WGS84'].includes( - this.projection - ); - if (!custom) { - // this is worth a read, because dispatchEvent is synchronous - // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent - // In particular: - // "All applicable event handlers are called and return before dispatchEvent() returns." - this.dispatchEvent(new CustomEvent('createmap')); - } - this._toggleStatic(); + var s = window.getComputedStyle(this), + wpx = s.width, + hpx = s.height, + w = this.hasAttribute('width') + ? this.getAttribute('width') + : parseInt(wpx.replace('px', '')), + h = this.hasAttribute('height') + ? this.getAttribute('height') + : parseInt(hpx.replace('px', '')); + this._changeWidth(w); + this._changeHeight(h); - /* + this._createMap(); + + this._toggleStatic(); + + /* 1. only deletes aria-label when the last (only remaining) map caption is removed 2. only deletes aria-label if the aria-label was defined by the map caption element itself */ - let mapcaption = this.querySelector('map-caption'); - - if (mapcaption !== null) { - setTimeout(() => { - let ariaupdate = this.getAttribute('aria-label'); - - if (ariaupdate === mapcaption.innerHTML) { - this.mapCaptionObserver = new MutationObserver((m) => { - let mapcaptionupdate = this.querySelector('map-caption'); - if (mapcaptionupdate !== mapcaption) { - this.removeAttribute('aria-label'); + let mapcaption = this.querySelector('map-caption'); + + if (mapcaption !== null) { + setTimeout(() => { + let ariaupdate = this.getAttribute('aria-label'); + + if (ariaupdate === mapcaption.innerHTML) { + this.mapCaptionObserver = new MutationObserver((m) => { + let mapcaptionupdate = this.querySelector('map-caption'); + if (mapcaptionupdate !== mapcaption) { + this.removeAttribute('aria-label'); + } + }); + this.mapCaptionObserver.observe(this, { + childList: true + }); } - }); - this.mapCaptionObserver.observe(this, { - childList: true - }); + }, 0); } - }, 0); - } + }) + .catch(() => { + throw new Error('Projection not defined'); + }); } _initShadowRoot() { let tmpl = document.createElement('template'); @@ -293,13 +282,7 @@ export class WebMap extends HTMLMapElement { mapEl: this, crs: M[this.projection], zoom: this.zoom, - zoomControl: false, - // because the M.MapMLLayer invokes _tileLayer._onMoveEnd when - // the mapml response is received the screen tends to flash. I'm sure - // there is a better configuration than that, but at this moment - // I'm not sure how to approach that issue. - // See https://github.com/Maps4HTML/MapML-Leaflet-Client/issues/24 - fadeAnimation: true + zoomControl: false }); this._addToHistory(); @@ -407,6 +390,48 @@ export class WebMap extends HTMLMapElement { case 'static': this._toggleStatic(); break; + case 'projection': + const reconnectLayers = () => { + if (this._map && this._map.options.projection !== newValue) { + // save map location and zoom + let lat = this.lat; + let lon = this.lon; + let zoom = this.zoom; + // saving the lat, lon and zoom is necessary because Leaflet seems + // to try to compensate for the change in the scales for each zoom + // level in the crs by changing the zoom level of the map when + // you set the map crs. So, we save the current view for use below + // when all the layers' reconnections have settled. + this._map.options.crs = M[newValue]; + this._map.options.projection = newValue; + let layersReady = []; + this._map.announceMovement.disable(); + for (let layer of this.querySelectorAll('layer-')) { + layer.removeAttribute('disabled'); + let reAttach = this.removeChild(layer); + this.appendChild(reAttach); + layersReady.push(reAttach.whenReady()); + } + Promise.allSettled(layersReady).then(() => { + // use the saved map location to ensure it is correct after + // changing the map CRS. Specifically affects projection + // upgrades, e.g. https://maps4html.org/experiments/custom-projections/BNG/ + this.zoomTo(lat, lon, zoom); + this._resetHistory(); + this._map.announceMovement.enable(); + }); + } + }; + if (newValue) { + const connect = reconnectLayers.bind(this); + new Promise((resolve, reject) => { + connect(); + resolve(); + }).then(() => { + if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); + }); + } + break; } } @@ -860,7 +885,7 @@ export class WebMap extends HTMLMapElement { this._updateMapCenter(); this._addToHistory(); this.dispatchEvent( - new CustomEvent('moveend', { detail: { target: this } }) + new CustomEvent('map-moveend', { detail: { target: this } }) ); }, this @@ -981,7 +1006,13 @@ export class WebMap extends HTMLMapElement { this.lon = this._map.getCenter().lng; this.zoom = this._map.getZoom(); } - + _resetHistory() { + this._history = []; + this._historyIndex = -1; + this._traversalCall = false; + // weird but ok + this._addToHistory(); + } /** * Adds to the maps history on moveends * @private @@ -1132,6 +1163,7 @@ export class WebMap extends HTMLMapElement { this._traversalCall = 1; this._map.panBy([initialLocation.x - curr.x, initialLocation.y - curr.y]); } + this._map.getContainer().focus(); } _toggleFullScreen() { @@ -1333,7 +1365,60 @@ export class WebMap extends HTMLMapElement { M[t.projection.toUpperCase()] = M[t.projection]; //adds the projection uppercase to global M return t.projection; } - + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this._map) { + resolve(); + } else { + let viewer = this; + interval = setInterval(testForMap, 200, viewer); + failureTimer = setTimeout(mapNotDefined, 5000); + } + function testForMap(viewer) { + if (viewer._map) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function mapNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for map to be ready'); + } + }); + } + async whenLayersReady() { + let layersReady = []; + for (let layer of [...this.layers]) { + layersReady.push(layer.whenReady()); + } + return Promise.allSettled(layersReady); + } + whenProjectionDefined(projection) { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (M[projection]) { + resolve(); + } else { + interval = setInterval(testForProjection, 200, projection); + failureTimer = setTimeout(projectionNotDefined, 5000); + } + function testForProjection(p) { + if (M[p]) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function projectionNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for projection to be defined'); + } + }); + } geojson2mapml(json, options = {}) { if (options.projection === undefined) { options.projection = this.projection; diff --git a/test/e2e/api/domApi-HTMLLayerElement.html b/test/e2e/api/domApi-HTMLLayerElement.html index 2978270b2..311fae6b4 100644 --- a/test/e2e/api/domApi-HTMLLayerElement.html +++ b/test/e2e/api/domApi-HTMLLayerElement.html @@ -42,40 +42,23 @@

A Man With Two Hats

- \ No newline at end of file diff --git a/test/e2e/api/domApi-HTMLLayerElement.test.js b/test/e2e/api/domApi-HTMLLayerElement.test.js index 8c63847c8..4b2b35b5e 100644 --- a/test/e2e/api/domApi-HTMLLayerElement.test.js +++ b/test/e2e/api/domApi-HTMLLayerElement.test.js @@ -16,10 +16,17 @@ test.describe('HTMLLayerElement DOM API Tests', () => { await context.close(); }); test('Setting HTMLLayerElement.label sets the layer name per spec', async () => { + const viewer = await page.locator('mapml-viewer'); + await viewer.evaluate((viewer) => { + return viewer.whenLayersReady(); + }); + await page.waitForTimeout(200); let remoteWithTitleLabel = await page.evaluate(() => { return document.querySelector('#remote-with-title').label; }); - expect(remoteWithTitleLabel).toEqual('Unforsettable in every way'); + expect(remoteWithTitleLabel).toEqual( + 'MapML author-controlled name - unsettable' + ); let remoteWithTitleName = await page.evaluate(() => { let layer = document.querySelector('#remote-with-title'); return layer._layer.getName(); @@ -41,15 +48,17 @@ test.describe('HTMLLayerElement DOM API Tests', () => { let localWithTitleLabel = await page.evaluate(() => { return document.querySelector('#local-with-title').label; }); - expect(localWithTitleLabel).toEqual('No dice, buddy!'); + expect(localWithTitleLabel).toEqual( + 'Layer name set via local map-title element - unsettable via HTMLLayerelement.label' + ); let localWithTitleName = await page.evaluate(() => { let layer = document.querySelector('#local-with-title'); return layer._layer.getName(); }); - expect(localWithTitleName).not.toEqual(localWithTitleLabel); + expect(localWithTitleName).toEqual(localWithTitleLabel); - // THIS SHOULD NOT BE NECESSARY, BUT IT IS see comment below - await page.waitForTimeout(500); + // // THIS SHOULD NOT BE NECESSARY, BUT IT IS see comment below + // await page.waitForTimeout(500); let localNoTitleLabel = await page.evaluate(() => { return document.querySelector('#local-no-title').label; }); diff --git a/test/e2e/api/domApi-mapml-viewer.test.js b/test/e2e/api/domApi-mapml-viewer.test.js index a6ecfc224..36b019dfe 100644 --- a/test/e2e/api/domApi-mapml-viewer.test.js +++ b/test/e2e/api/domApi-mapml-viewer.test.js @@ -108,11 +108,8 @@ test.describe('mapml-viewer DOM API Tests', () => { (layer) => document.querySelector('mapml-viewer').appendChild(layer), layerHandle ); - let layerControlHidden = await page.$eval( - 'css=body > mapml-viewer >> css=div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div', - (elem) => elem.hasAttribute('hidden') - ); - expect(layerControlHidden).toEqual(false); + let layerControl = await page.locator('.leaflet-control-layers'); + await expect(layerControl).toBeVisible(); // set the layer's hidden attribute, the layer should be removed from the layer // control (but not the map), which leaves 0 layers in the layer control, which means the @@ -121,11 +118,7 @@ test.describe('mapml-viewer DOM API Tests', () => { (layer) => layer.setAttribute('hidden', ''), layerHandle ); - layerControlHidden = await page.$eval( - 'css=body > mapml-viewer >> css=div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div', - (elem) => elem.hasAttribute('hidden') - ); - expect(layerControlHidden).toEqual(true); + await expect(layerControl).toBeHidden(); // takes a couple of seconds for the tiles to load @@ -138,6 +131,7 @@ test.describe('mapml-viewer DOM API Tests', () => { }); test('Remove mapml-viewer from DOM, add it back in', async () => { + await page.pause(); // check for error messages in console let errorLogs = []; page.on('pageerror', (err) => { @@ -145,12 +139,18 @@ test.describe('mapml-viewer DOM API Tests', () => { }); // locators avoid flaky tests, allegedly const viewer = await page.locator('mapml-viewer'); - await viewer.evaluate(() => {}); + await viewer.evaluate(() => { + let m = document.querySelector('mapml-viewer'); + document.body.removeChild(m); + document.body.appendChild(m); + }); + await viewer.evaluate((viewer) => + viewer.querySelector('layer-').whenReady() + ); + await page.waitForTimeout(250); expect( await viewer.evaluate(() => { let m = document.querySelector('mapml-viewer'); - document.body.removeChild(m); - document.body.appendChild(m); let l = m.querySelector('layer-'); return l.label; // the label attribute is ignored if the mapml document has a map-title @@ -365,10 +365,7 @@ test.describe('mapml-viewer DOM API Tests', () => { '.leaflet-top.leaflet-left > .leaflet-control-fullscreen', (div) => div.hidden ); - let layerControlHidden = await page.$eval( - '.leaflet-top.leaflet-right > .leaflet-control-layers', - (div) => div.hidden - ); + let layerControl = await page.locator('.leaflet-control-layers'); let scaleHidden = await page.$eval( '.leaflet-bottom.leaflet-left > .mapml-control-scale', (div) => div.hidden @@ -381,7 +378,7 @@ test.describe('mapml-viewer DOM API Tests', () => { expect(zoomHidden).toEqual(true); expect(reloadHidden).toEqual(true); expect(fullscreenHidden).toEqual(true); - expect(layerControlHidden).toEqual(true); + await expect(layerControl).toBeHidden(); expect(scaleHidden).toEqual(true); }); @@ -1111,8 +1108,15 @@ test.describe('mapml-viewer DOM API Tests', () => { (viewer) => viewer.setAttribute('height', '600'), viewerHandle ); + + // Adding custom projection + const custProj = await page.evaluate((viewer) => { + return viewer.defineCustomProjection(template); + }, viewerHandle); + expect(custProj).toEqual('basic'); + await page.evaluateHandle( - (viewer) => viewer.setAttribute('projection', 'other'), + (viewer) => viewer.setAttribute('projection', 'basic'), viewerHandle ); await page.evaluateHandle( @@ -1120,11 +1124,6 @@ test.describe('mapml-viewer DOM API Tests', () => { viewerHandle ); - // Adding custom projection - const custProj = await page.evaluate((viewer) => { - return viewer.defineCustomProjection(template); - }, viewerHandle); - expect(custProj).toEqual('basic'); await page.evaluate((viewer) => { viewer.projection = 'basic'; }, viewerHandle); diff --git a/test/e2e/api/domApi-web-map.test.js b/test/e2e/api/domApi-web-map.test.js index cd906c824..65a528912 100644 --- a/test/e2e/api/domApi-web-map.test.js +++ b/test/e2e/api/domApi-web-map.test.js @@ -103,11 +103,8 @@ test.describe('web-map DOM API Tests', () => { (layer) => document.querySelector('map').appendChild(layer), layerHandle ); - let layerControlHidden = await page.$eval( - 'css=body > map >> css=div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div', - (elem) => elem.hasAttribute('hidden') - ); - expect(layerControlHidden).toEqual(false); + let layerControl = await page.locator('.leaflet-control-layers'); + await expect(layerControl).toBeVisible(); // set the layer's hidden attribute, the layer should be removed from the layer // control (but not the map), which leaves 0 layers in the layer control, which means the @@ -116,11 +113,7 @@ test.describe('web-map DOM API Tests', () => { (layer) => layer.setAttribute('hidden', ''), layerHandle ); - layerControlHidden = await page.$eval( - 'css=body > map >> css=div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div', - (elem) => elem.hasAttribute('hidden') - ); - expect(layerControlHidden).toEqual(true); + await expect(layerControl).toBeHidden(); // takes a couple of seconds for the tiles to load @@ -139,12 +132,18 @@ test.describe('web-map DOM API Tests', () => { }); // locators avoid flaky tests, allegedly const viewer = await page.locator('map'); - await viewer.evaluate(() => {}); + await viewer.evaluate(() => { + let m = document.querySelector('map'); + document.body.removeChild(m); + document.body.appendChild(m); + }); + await viewer.evaluate((viewer) => + viewer.querySelector('layer-').whenReady() + ); + await page.waitForTimeout(250); expect( await viewer.evaluate(() => { let m = document.querySelector('map'); - document.body.removeChild(m); - document.body.appendChild(m); let l = m.querySelector('layer-'); return l.label; // the label attribute is ignored if the mapml document has a map-title @@ -346,10 +345,7 @@ test.describe('web-map DOM API Tests', () => { '.leaflet-top.leaflet-left > .leaflet-control-fullscreen', (div) => div.hidden ); - let layerControlHidden = await page.$eval( - '.leaflet-top.leaflet-right > .leaflet-control-layers', - (div) => div.hidden - ); + let layerControl = await page.locator('.leaflet-control-layers'); let scaleHidden = await page.$eval( '.leaflet-bottom.leaflet-left > .mapml-control-scale', (div) => div.hidden @@ -362,7 +358,7 @@ test.describe('web-map DOM API Tests', () => { expect(zoomHidden).toEqual(true); expect(reloadHidden).toEqual(true); expect(fullscreenHidden).toEqual(true); - expect(layerControlHidden).toEqual(true); + await expect(layerControl).toBeHidden(); expect(scaleHidden).toEqual(true); }); @@ -1094,8 +1090,15 @@ test.describe('web-map DOM API Tests', () => { (viewer) => viewer.setAttribute('height', '600'), mapHandle ); + + // Adding custom projection + const custProj = await page.evaluate((viewer) => { + return viewer.defineCustomProjection(template); + }, mapHandle); + expect(custProj).toEqual('basic'); + await page.evaluateHandle( - (viewer) => viewer.setAttribute('projection', 'other'), + (viewer) => viewer.setAttribute('projection', 'basic'), mapHandle ); await page.evaluateHandle( @@ -1103,11 +1106,6 @@ test.describe('web-map DOM API Tests', () => { mapHandle ); - // Adding custom projection - const custProj = await page.evaluate((viewer) => { - return viewer.defineCustomProjection(template); - }, mapHandle); - expect(custProj).toEqual('basic'); await page.evaluate((viewer) => { viewer.projection = 'basic'; }, mapHandle); diff --git a/test/e2e/api/locateApi.test.js b/test/e2e/api/locateApi.test.js index eb76d56b0..a2af63e66 100644 --- a/test/e2e/api/locateApi.test.js +++ b/test/e2e/api/locateApi.test.js @@ -81,13 +81,7 @@ test.describe('Locate API Test', () => { test('Testing API when the button is used', async () => { await page.reload(); await page.click('body > mapml-viewer'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Enter'); + await page.getByTitle('Show my location - location tracking off').click(); await page.mouse.move(600, 300); await page.mouse.down(); diff --git a/test/e2e/core/debugMode.test.js b/test/e2e/core/debugMode.test.js index 6f3e1efc3..339b84116 100644 --- a/test/e2e/core/debugMode.test.js +++ b/test/e2e/core/debugMode.test.js @@ -40,7 +40,7 @@ test.describe('Playwright Map Element Tests', () => { test('Reasonable debug layer extent created', async () => { const feature = await page.$eval( - 'xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > svg > g > path:nth-child(2)', + 'xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > svg > g > path[stroke="#8DFF33"]', (tile) => tile.getAttribute('d') ); expect(feature).toEqual( @@ -50,7 +50,7 @@ test.describe('Playwright Map Element Tests', () => { test('Large debug layer extent created', async () => { const feature = await page.$eval( - 'xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > svg > g > path:nth-child(4)', + 'xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > svg > g > path[stroke="#E433FF"]', (tile) => tile.getAttribute('d') ); expect(feature).toEqual('M-659 500L365 500L365 -780L-659 -780z'); @@ -58,7 +58,7 @@ test.describe('Playwright Map Element Tests', () => { test('Debug layer extent beyond ((0,0), (5,5)) created', async () => { const feature = await page.$eval( - 'xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > svg > g > path:nth-child(6)', + 'xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > svg > g > path[d="M-1683 1268L1133 1268L1133 -1292L-1683 -1292z"]', (tile) => tile.getAttribute('d') ); expect(feature).toEqual('M-1683 1268L1133 1268L1133 -1292L-1683 -1292z'); diff --git a/test/e2e/core/drag.test.js b/test/e2e/core/drag.test.js index 2c5f0f2ed..8a94878fd 100644 --- a/test/e2e/core/drag.test.js +++ b/test/e2e/core/drag.test.js @@ -73,8 +73,8 @@ test.describe('UI Drag&Drop Test', () => { (span) => span.innerText ); const layerIndex = await page.$eval( - '.leaflet-pane.leaflet-overlay-pane > div:nth-child(1)', - (div) => div.style.zIndex + '.leaflet-pane.leaflet-overlay-pane .mapml-templated-tile-container', + (div) => div.parentElement.parentElement.style.zIndex ); const domLayer = await page.$eval( 'body > map > layer-:nth-child(4)', @@ -109,8 +109,8 @@ test.describe('UI Drag&Drop Test', () => { (span) => span.innerText ); const layerIndex = await page.$eval( - '.leaflet-overlay-pane > div:nth-child(2)', - (div) => div.style.zIndex + '.leaflet-overlay-pane .mapml-static-tile-layer', + (div) => div.parentElement.style.zIndex ); const domLayer = await page.$eval( 'map > layer-:nth-child(3)', diff --git a/test/e2e/core/featureIndexOverlay.html b/test/e2e/core/featureIndexOverlay.html index 417f9ab87..f6226545e 100644 --- a/test/e2e/core/featureIndexOverlay.html +++ b/test/e2e/core/featureIndexOverlay.html @@ -14,11 +14,10 @@ - + - + @@ -36,6 +35,27 @@
+ + + + + + + + + + + Test link + + + + -75.705278 45.397778 + + + + + + diff --git a/test/e2e/core/featureIndexOverlayFocus.test.js b/test/e2e/core/featureIndexOverlayFocus.test.js new file mode 100644 index 000000000..a3455d3c7 --- /dev/null +++ b/test/e2e/core/featureIndexOverlayFocus.test.js @@ -0,0 +1,162 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.use({ + geolocation: { longitude: -75.705278, latitude: 45.397778 }, + permissions: ['geolocation'] +}); + +test.describe('Feature Index Overlay Focus tests', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext('', { + headless: false, + slowMo: 250 + }); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('featureIndexOverlay.html'); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test('Feature index overlay and reticle shows on focus', async () => { + const hiddenOverlay = await page.$eval( + 'div > output.mapml-feature-index', + (output) => output.classList.contains('mapml-screen-reader-output') + ); + const hiddenReticle = await page.$eval( + 'div > div.mapml-feature-index-box', + (div) => div.hasAttribute('hidden') + ); + + await page.keyboard.press('Tab'); + await page.waitForTimeout(500); + const afterTabOverlay = await page.$eval( + 'div > output.mapml-feature-index', + (output) => output.classList.contains('mapml-screen-reader-output') + ); + const afterTabReticle = await page.$eval( + 'div > div.mapml-feature-index-box', + (div) => div.hasAttribute('hidden') + ); + + expect(hiddenOverlay).toEqual(true); + expect(hiddenReticle).toEqual(true); + expect(afterTabOverlay).toEqual(false); + expect(afterTabReticle).toEqual(false); + }); + test('Feature index overlay and reticle show on fullscreen', async () => { + await page.locator('#map1').getByTitle('View Fullscreen').click(); + const afterFullscreenReticle = page.locator( + '#map1 .mapml-feature-index-box' + ); + expect(await afterFullscreenReticle.isHidden()).toBe(false); + + const afterFullscreenOutput = page.locator( + '#map1 output.mapml-feature-index' + ); + expect( + await afterFullscreenOutput.evaluate((o) => + o.classList.contains('mapml-screen-reader-output') + ) + ).toBe(false); + await page.locator('#map1').getByTitle('Exit Fullscreen').click(); + }); + test('Feature index overlay and reticle show on reload', async () => { + await page.keyboard.press('ArrowRight'); + await page.locator('#map1').getByTitle('Reload').click(); + const afterReloadReticle = page.locator('#map1 .mapml-feature-index-box'); + expect(await afterReloadReticle.isHidden()).toBe(false); + + const afterReloadOutput = page.locator('#map1 output.mapml-feature-index'); + expect( + await afterReloadOutput.evaluate((o) => + o.classList.contains('mapml-screen-reader-output') + ) + ).toBe(false); + }); + test('Feature index overlay and reticle show on history-based navigation', async () => { + await page.locator('#map1').focus(); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('Shift+F10'); + await page.locator('#map1').getByText('Back').click(); + await page.keyboard.press('Shift+F10'); + await page.locator('#map1').getByText('Back').click(); + const afterHistoryNavReticle = await page.locator( + '#map1 .mapml-feature-index-box' + ); + expect(await afterHistoryNavReticle.isHidden()).toBe(false); + const afterHistoryNavOutput = page.locator( + '#map1 output.mapml-feature-index' + ); + expect( + await afterHistoryNavOutput.evaluate((o) => + o.classList.contains('mapml-screen-reader-output') + ) + ).toBe(false); + //await page.locator('#map1').getByTitle('Reload').click(); + }); + test('Feature index overlay and reticle show on geolocation activation, deactivation', async () => { + await page + .locator('#map3') + .getByTitle('Show my location - location tracking off') + .click(); + const afterGeolocationStartReticle = page.locator( + '#map3 .mapml-feature-index-box' + ); + expect(await afterGeolocationStartReticle.isHidden()).toBe(false); + + const afterGeolocationStartOutput = page.locator( + '#map3 output.mapml-feature-index' + ); + expect( + await afterGeolocationStartOutput.evaluate((o) => + o.classList.contains('mapml-screen-reader-output') + ) + ).toBe(false); + + await page + .locator('#map3') + .getByTitle('Show my location - location tracking on') + .click(); + const afterGeolocationStopReticle = page.locator( + '#map3 .mapml-feature-index-box' + ); + expect(await afterGeolocationStopReticle.isHidden()).toBe(false); + + const afterGeolocationStopOutput = page.locator( + '#map3 output.mapml-feature-index' + ); + expect( + await afterGeolocationStopOutput.evaluate((o) => + o.classList.contains('mapml-screen-reader-output') + ) + ).toBe(false); + await page.locator('#map3').getByTitle('Reload').click(); + }); + test('Feature index overlay and reticle show after following a link', async () => { + await page.locator('#map3').scrollIntoViewIfNeeded(); + await page.locator('#map3 .leaflet-interactive.map-a').click(); + const afterFollowingLinkReticle = page.locator( + '#map3 .mapml-feature-index-box' + ); + expect(await afterFollowingLinkReticle.isHidden()).toBe(false); + + const afterFollowingLinkOutput = page.locator( + '#map3 output.mapml-feature-index' + ); + expect( + await afterFollowingLinkOutput.evaluate((o) => + o.classList.contains('mapml-screen-reader-output') + ) + ).toBe(false); + await page.locator('#map3').getByTitle('Reload').click(); + }); +}); diff --git a/test/e2e/core/featureIndexOverlay.test.js b/test/e2e/core/featureIndexOverlayResults.test.js similarity index 77% rename from test/e2e/core/featureIndexOverlay.test.js rename to test/e2e/core/featureIndexOverlayResults.test.js index 8657fd396..2a5911a5b 100644 --- a/test/e2e/core/featureIndexOverlay.test.js +++ b/test/e2e/core/featureIndexOverlayResults.test.js @@ -1,6 +1,6 @@ import { test, expect, chromium } from '@playwright/test'; -test.describe('Feature Index Overlay test', () => { +test.describe('Feature Index Overlay results test', () => { let page; let context; test.beforeAll(async () => { @@ -15,34 +15,8 @@ test.describe('Feature Index Overlay test', () => { await context.close(); }); - test('Feature index overlay and reticle shows on focus', async () => { - const hiddenOverlay = await page.$eval( - 'div > output.mapml-feature-index', - (output) => output.classList.contains('mapml-screen-reader-output') - ); - const hiddenReticle = await page.$eval( - 'div > div.mapml-feature-index-box', - (div) => div.hasAttribute('hidden') - ); - - await page.keyboard.press('Tab'); - await page.waitForTimeout(500); - const afterTabOverlay = await page.$eval( - 'div > output.mapml-feature-index', - (output) => output.classList.contains('mapml-screen-reader-output') - ); - const afterTabReticle = await page.$eval( - 'div > div.mapml-feature-index-box', - (div) => div.hasAttribute('hidden') - ); - - expect(hiddenOverlay).toEqual(true); - expect(hiddenReticle).toEqual(true); - expect(afterTabOverlay).toEqual(false); - expect(afterTabReticle).toEqual(false); - }); - test('Feature index content is correct', async () => { + await page.keyboard.press('Tab'); const spanCount = await page.$eval( 'div > output.mapml-feature-index > span', (span) => span.childElementCount @@ -109,21 +83,26 @@ test.describe('Feature Index Overlay test', () => { expect(firstFeature).toContain('1 Maine'); }); - test('Feature index overlay is hidden when empty, reticle still visible', async () => { + test('Feature index message for "No features found", reticle still visible', async () => { await page.keyboard.press('ArrowUp'); await page.waitForTimeout(1000); - const overlay = await page.$eval( + const overlayVisible = await page.$eval( 'div > output.mapml-feature-index', - (output) => output.classList.contains('mapml-screen-reader-output') + (output) => !output.classList.contains('mapml-screen-reader-output') ); - const reticle = await page.$eval( + const reticleVisible = await page.$eval( 'div > div.mapml-feature-index-box', - (div) => div.hasAttribute('hidden') + (div) => !div.hasAttribute('hidden') + ); + const message = await page.$eval( + '.mapml-feature-index-content > span', + (message) => message.textContent ); - expect(overlay).toEqual(true); - expect(reticle).toEqual(false); + expect(overlayVisible).toEqual(true); + expect(reticleVisible).toEqual(true); + expect(message).toEqual('No features found'); }); test('Popup test with templated features', async () => { diff --git a/test/e2e/core/layerAttributes.test.js b/test/e2e/core/layerAttributes.test.js index 326a20559..288a4acd9 100644 --- a/test/e2e/core/layerAttributes.test.js +++ b/test/e2e/core/layerAttributes.test.js @@ -101,22 +101,19 @@ test.describe('Playwright Checked Attribute Tests', () => { test.describe('Opacity setters & getters test', () => { test('Setting opacity', async () => { await page.reload(); - await page.$eval( - 'body > mapml-viewer > layer-', - (layer) => (layer.opacity = 0.4) - ); - let value = await page.$eval( - 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset > div:nth-child(2) > details > input[type=range]', - (input) => input.value + const layer = page.getByTestId('testlayer'); + await layer.evaluate((layer) => layer.whenReady()); + await layer.evaluate((layer) => (layer.opacity = 0.4)); + let value = await layer.evaluate( + (layer) => + layer._layer._mapmlLayerItem.querySelector('input[type=range]').value ); expect(value).toEqual('0.4'); }); test('Getting appropriate opacity', async () => { - let value = await page.$eval( - 'body > mapml-viewer > layer-', - (layer) => layer.opacity - ); + const layer = page.getByTestId('testlayer'); + let value = await layer.evaluate((layer) => layer.opacity); expect(value).toEqual('0.4'); }); }); diff --git a/test/e2e/core/layerContextMenu.test.js b/test/e2e/core/layerContextMenu.test.js index c1173feda..27ff8ede0 100644 --- a/test/e2e/core/layerContextMenu.test.js +++ b/test/e2e/core/layerContextMenu.test.js @@ -45,6 +45,7 @@ test.describe('Playwright Layer Context Menu Tests', () => { ).jsonValue(); expect(menuDisplay).toEqual('block'); + await page.keyboard.press('Escape'); }); test('Layer context menu copy layer', async () => { @@ -65,7 +66,7 @@ test.describe('Playwright Layer Context Menu Tests', () => { ); expect(copyLayer).toEqual( - '\n \n \n ' + '\n \n \n ' ); }); @@ -157,6 +158,7 @@ test.describe('Playwright Layer Context Menu Tests', () => { }); test('Copy layer with relative src attribute', async () => { + await page.reload(); await page.hover( 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div' ); @@ -167,8 +169,10 @@ test.describe('Playwright Layer Context Menu Tests', () => { await page.keyboard.press('l'); await page.click('body > textarea#messageLayer'); - await page.keyboard.press('Control+a'); - await page.keyboard.press('Backspace'); + // reload is better than deleting text, because of cross-platform issue + // with copy-pasting text on Windows/Linux + // await page.keyboard.press('Control+a'); + // await page.keyboard.press('Backspace'); await page.keyboard.press('Control+v'); const copyLayer = await page.$eval( 'body > textarea#messageLayer', diff --git a/test/e2e/core/mapContextMenu.test.js b/test/e2e/core/mapContextMenu.test.js index 1b20612a6..508e8ef54 100644 --- a/test/e2e/core/mapContextMenu.test.js +++ b/test/e2e/core/mapContextMenu.test.js @@ -328,7 +328,8 @@ test.describe('Playwright Map Context Menu Tests', () => { test('Submenu, copy map (MapML)', async () => { await page.reload(); - await page.waitForTimeout(3000); + const map = await page.getByTestId('firstmap'); + await map.evaluate((map) => map.whenLayersReady()); await page.click('body > map'); await page.keyboard.press('Shift+F10'); await page.keyboard.press('Tab'); @@ -341,9 +342,9 @@ test.describe('Playwright Map Context Menu Tests', () => { 'body > textarea#coord', (text) => text.value ); - const expected = ` + const expected = ` - - + @@ -43,7 +43,7 @@ - + diff --git a/test/e2e/core/metaDefault.test.js b/test/e2e/core/metaDefault.test.js index 7a74ff10b..77bf28e95 100644 --- a/test/e2e/core/metaDefault.test.js +++ b/test/e2e/core/metaDefault.test.js @@ -91,17 +91,15 @@ test.describe('Playwright Missing Min Max Attribute, Meta Default Tests', () => ); }); test("Layer with no map-meta's is rendered on map", async () => { - const viewer = await page.evaluateHandle(() => - document.querySelector('mapml-viewer') + await page.waitForTimeout(200); + const layer = await page.evaluateHandle(() => + document.querySelector('layer-[id=defaultMeta]') ); const layerSVG = await ( await page.evaluateHandle( - (map) => - map.shadowRoot - .querySelectorAll('.mapml-layer')[2] - .querySelector('path') - .getAttribute('d'), - viewer + (layer) => + layer._layer._container.querySelector('path').getAttribute('d'), + layer ) ).jsonValue(); expect(layerSVG).toEqual( @@ -109,17 +107,14 @@ test.describe('Playwright Missing Min Max Attribute, Meta Default Tests', () => ); }); test("Fetched layer with no map-meta's is rendered on map", async () => { - const viewer = await page.evaluateHandle(() => - document.querySelector('mapml-viewer') + const layer = await page.evaluateHandle(() => + document.querySelector('layer-[id=defaultMetaFetched]') ); const layerSVG = await ( await page.evaluateHandle( - (map) => - map.shadowRoot - .querySelectorAll('.mapml-layer')[3] - .querySelector('path') - .getAttribute('d'), - viewer + (layer) => + layer._layer._container.querySelector('path').getAttribute('d'), + layer ) ).jsonValue(); expect(layerSVG).toEqual( diff --git a/test/e2e/core/projectionChange.test.js b/test/e2e/core/projectionChange.test.js index fbaec5e2e..7f83e370e 100644 --- a/test/e2e/core/projectionChange.test.js +++ b/test/e2e/core/projectionChange.test.js @@ -56,6 +56,7 @@ test.describe('Playwright Projection Change Tests', () => { test('Debug components update with projection changes', async () => { await page.reload(); + await page.waitForTimeout(500); await page.$eval('body > map:nth-child(1)', (map) => map.toggleDebug()); const colBefore = await page.$eval( @@ -81,7 +82,8 @@ test.describe('Playwright Projection Change Tests', () => { await page.waitForTimeout(200); } await page.keyboard.press('Enter'); - await page.waitForTimeout(2000); + // enter on the wrong thing + await page.waitForTimeout(1000); const colAfter = await page.$eval( 'xpath=//html/body/map[1] >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-layer.mapml-debug-grid > div > div:nth-child(1)', @@ -104,13 +106,13 @@ test.describe('Playwright Projection Change Tests', () => { expect(colBefore).toEqual('10'); expect(rowBefore).toEqual('11'); expect(zoomBefore).toEqual('2'); - expect(colAfter).toEqual('0'); - expect(rowAfter).toEqual('0'); - expect(zoomAfter).toEqual('0'); + expect(colAfter).toEqual('1'); + expect(rowAfter).toEqual('1'); + expect(zoomAfter).toEqual('2'); expect(centerBefore).toEqual( 'M132.64578432000008,238.45862407874074a1,1 0 1,0 2,0 a1,1 0 1,0 -2,0 ' ); - expect(centerAfter).toEqual('M249,250a1,1 0 1,0 2,0 a1,1 0 1,0 -2,0 '); + expect(centerAfter).toEqual('M463,396a1,1 0 1,0 2,0 a1,1 0 1,0 -2,0 '); }); }); }); diff --git a/test/e2e/core/projectionDefault.html b/test/e2e/core/projectionDefault.html new file mode 100644 index 000000000..e1edab2dc --- /dev/null +++ b/test/e2e/core/projectionDefault.html @@ -0,0 +1,26 @@ + + + + + + + + Default Projection + + + + + + Rectangle + + + -123.216259390223 55.90361621419453 -123.216259390223 -0.5886925650467134 78.15130158758717 -0.5886925650467134 78.15130158758717 55.90361621419453 -123.216259390223 55.90361621419453 + + + + Rectangle + + + + + \ No newline at end of file diff --git a/test/e2e/core/projectionDefault.test.js b/test/e2e/core/projectionDefault.test.js new file mode 100644 index 000000000..91ebcd856 --- /dev/null +++ b/test/e2e/core/projectionDefault.test.js @@ -0,0 +1,50 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('Playwright Viewer Default Projection', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext(''); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('projectionDefault.html'); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test.describe('Viewer with no projection attribute', () => { + test('Viewer defaults to OSMTILE', async () => { + const mapProjection = await page.$eval( + 'body > mapml-viewer', + (map) => map.projection + ); + const leafletProjection = await page.$eval( + 'body > mapml-viewer', + (map) => map._map.options.projection + ); + const leafletProjection1 = await page.$eval( + 'body > mapml-viewer', + (map) => map._map.options.crs.code + ); + const projectionAttribute = await page.$eval( + 'body > mapml-viewer', + (map) => map.getAttribute('projection') + ); + expect(mapProjection).toEqual('OSMTILE'); + expect(leafletProjection).toEqual('OSMTILE'); + expect(leafletProjection1).toEqual('EPSG:3857'); + expect(projectionAttribute).toEqual(null); + }); + + test('layer renders', async () => { + const featureSVG = await page.$eval( + 'body > mapml-viewer > layer- > map-feature', + (feature) => feature._groupEl.firstChild.getAttribute('d') + ); + expect(featureSVG).toEqual('M62 27L62 75L206 75L206 27L62 27z'); + }); + }); +}); diff --git a/test/e2e/data/images/toporama_en.jpg b/test/e2e/data/images/toporama_en.jpg new file mode 100644 index 000000000..3cc48026c Binary files /dev/null and b/test/e2e/data/images/toporama_en.jpg differ diff --git a/test/e2e/data/tiles/cbmt/templatedImage.mapml b/test/e2e/data/tiles/cbmt/templatedImage.mapml index 2b410a230..43ca2a2b1 100644 --- a/test/e2e/data/tiles/cbmt/templatedImage.mapml +++ b/test/e2e/data/tiles/cbmt/templatedImage.mapml @@ -14,7 +14,7 @@ - +
diff --git a/test/e2e/geojson/geojson2mapml.test.js b/test/e2e/geojson/geojson2mapml.test.js index 3a4c24ae9..711915e6b 100644 --- a/test/e2e/geojson/geojson2mapml.test.js +++ b/test/e2e/geojson/geojson2mapml.test.js @@ -9,7 +9,7 @@ test.describe('GeoJSON API - geojson2mapml', () => { context.pages().find((page) => page.url() === 'about:blank') || (await context.newPage()); await page.goto('geojson2mapml.html'); - //await page.waitForTimeout(10000); + await page.waitForTimeout(200); }); test.afterAll(async function () { diff --git a/test/e2e/layers/CustomProjectionLayers.test.js b/test/e2e/layers/CustomProjectionLayers.test.js index 751192a21..4e514a039 100644 --- a/test/e2e/layers/CustomProjectionLayers.test.js +++ b/test/e2e/layers/CustomProjectionLayers.test.js @@ -9,6 +9,8 @@ test.describe('Custom Projection Feature & Extent Tests', () => { context.pages().find((page) => page.url() === 'about:blank') || (await context.newPage()); await page.goto('CustomProjectionLayers.html'); + const map = page.locator('mapml-viewer'); + await map.evaluate((map) => map.whenLayersReady()); }); test.afterAll(async function () { diff --git a/test/e2e/layers/featureLayer.html b/test/e2e/layers/featureLayer.html index aaa5095ea..b3e1bf21e 100644 --- a/test/e2e/layers/featureLayer.html +++ b/test/e2e/layers/featureLayer.html @@ -133,8 +133,8 @@

Colorado

title="Canada Base Map © Natural Resources Canada">
- - + + diff --git a/test/e2e/layers/featureLayer.test.js b/test/e2e/layers/featureLayer.test.js index d1fa21bf4..363a4e132 100644 --- a/test/e2e/layers/featureLayer.test.js +++ b/test/e2e/layers/featureLayer.test.js @@ -44,9 +44,12 @@ test.describe('Playwright featureLayer (Static Features) Layer Tests', () => { }); test('Loading in retrieved features', async () => { + await page.waitForTimeout(200); const features = await page.$eval( - 'xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(3) > div.leaflet-layer.leaflet-pane.mapml-vector-container > svg > g', - (featureGroups) => featureGroups.childNodes.length + 'layer-#US', + (layer) => + layer._layer._container.querySelector('svg').firstChild + .childElementCount ); expect(features).toEqual(52); }); @@ -120,13 +123,13 @@ test.describe('Playwright featureLayer (Static Features) Layer Tests', () => { await context.close(); }); test('Feature without properties renders & is not interactable', async () => { - const feature = await page.$eval( - 'xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(4) > div.leaflet-layer.leaflet-pane.mapml-vector-container > svg > g > g > path', - (path) => path.getAttribute('d') + const feature = await page.$eval('layer-#inline', (layer) => + layer._layer._container.querySelector('path').getAttribute('d') ); - const classList = await page.$eval( - 'xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(4) > div.leaflet-layer.leaflet-pane.mapml-vector-container > svg > g > g', - (g) => g.getAttribute('class') + const classList = await page.$eval('layer-#inline', (layer) => + layer._layer._container + .querySelector('svg') + .firstChild.firstChild.getAttribute('class') ); expect(feature).toEqual('M74 -173L330 -173L330 83L74 83L74 -173z'); expect(classList).toBeFalsy(); diff --git a/test/e2e/layers/layerOpacityAttribute.test.js b/test/e2e/layers/layerOpacityAttribute.test.js index 3978ecb15..42ec0a2a1 100644 --- a/test/e2e/layers/layerOpacityAttribute.test.js +++ b/test/e2e/layers/layerOpacityAttribute.test.js @@ -9,39 +9,35 @@ test.describe('Adding Opacity Attribute to the Layer- Element', () => { context.pages().find((page) => page.url() === 'about:blank') || (await context.newPage()); await page.goto('layerOpacityAttribute.html'); + const l = await page.locator('layer-'); + await l.evaluate((l) => l.whenReady()); }); test.afterAll(async function () { await context.close(); }); test('Setting Opacity Attibute to Layer- Element', async () => { + await page.pause(); let opacity_attribute_value = await page.$eval( 'body > mapml-viewer > layer-', (layer) => layer.getAttribute('opacity') ); - if (!opacity_attribute_value) { - return; - } else { - let layer_opacity = await page.$eval( - 'body > mapml-viewer > layer-', - (layer) => layer.opacity - ); - expect(layer_opacity).toEqual(opacity_attribute_value); - } + let layer_opacity = await page.$eval( + 'body > mapml-viewer > layer-', + (layer) => layer.opacity + ); + expect(layer_opacity).toEqual(opacity_attribute_value); }); test('Opacity Slider Value Test', async () => { + await page.pause(); let opacity_slider_value = await page.$eval( 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset > div:nth-child(2) > details > input[type=range]', (input) => input.value ); - if (!opacity_slider_value) { - return; - } else { - let layer_opacity = await page.$eval( - 'body > mapml-viewer > layer-', - (layer) => layer.opacity - ); - expect(layer_opacity).toEqual(opacity_slider_value); - } + let layer_opacity = await page.$eval( + 'body > mapml-viewer > layer-', + (layer) => layer.opacity + ); + expect(layer_opacity).toEqual(opacity_slider_value); }); }); diff --git a/test/e2e/layers/multipleExtents.html b/test/e2e/layers/multipleExtents.html index b3d778e0f..4731e977b 100644 --- a/test/e2e/layers/multipleExtents.html +++ b/test/e2e/layers/multipleExtents.html @@ -49,7 +49,7 @@ + tref="images/toporama_en.jpg?SERVICE=WMS&REQUEST=GetMap&FORMAT=image/jpeg&TRANSPARENT=FALSE&STYLES=&VERSION=1.3.0&LAYERS=WMS-Toporama&WIDTH={w}&HEIGHT={h}&CRS=EPSG:3978&BBOX={xmin},{ymin},{xmax},{ymax}&m4h=t" >
diff --git a/test/e2e/layers/multipleQueryExtents.test.js b/test/e2e/layers/multipleQueryExtents.test.js index 5a57ae6f7..41cce8550 100644 --- a/test/e2e/layers/multipleQueryExtents.test.js +++ b/test/e2e/layers/multipleQueryExtents.test.js @@ -22,6 +22,7 @@ test.describe('Multiple Extent Query Tests', () => { await page.evaluateHandle(() => document.querySelector('mapml-viewer').zoomTo(85, 147, 0) ); + await page.waitForTimeout(1000); await page.click('mapml-viewer'); await page.waitForSelector('.leaflet-popup-content-wrapper p'); let numFeatures = await page.$eval( @@ -52,20 +53,12 @@ test.describe('Multiple Extent Query Tests', () => { test('Querying overlapping extents, user is able to navigate into second set of query results using popup controls', async () => { let feature; - let nextFeatureButton = - 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)'; - await page.click(nextFeatureButton); - // await page.waitForTimeout(500); - await page.click(nextFeatureButton); - // await page.waitForTimeout(500); - await page.click(nextFeatureButton); - // await page.waitForTimeout(500); - await page.click(nextFeatureButton); - // await page.waitForTimeout(500); - await page.click(nextFeatureButton); - // await page.waitForTimeout(500); - await page.click(nextFeatureButton); - // await page.waitForTimeout(500); + await page.getByTitle('Next Feature', { exact: true }).click(); + await page.getByTitle('Next Feature', { exact: true }).click(); + await page.getByTitle('Next Feature', { exact: true }).click(); + await page.getByTitle('Next Feature', { exact: true }).click(); + await page.getByTitle('Next Feature', { exact: true }).click(); + await page.getByTitle('Next Feature', { exact: true }).click(); const name = await page .frameLocator('iframe') @@ -87,10 +80,7 @@ test.describe('Multiple Extent Query Tests', () => { }); test("Navigate back from second query result set to end of first query result set by clicking '< / Previous'", async () => { - // click the '<' (previous) button in the popup. - await page.click( - 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(2)' - ); + await page.getByTitle('Previous Feature', { exact: true }).click(); const feature = await page.$eval( 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.mapml-vector-container > svg > g', (g) => (g.firstElementChild ? g.firstElementChild : false) @@ -114,10 +104,13 @@ test.describe('Multiple Extent Query Tests', () => { await page.evaluateHandle(() => document.querySelector('mapml-viewer').zoomTo(10, 5, 0) ); - await page.click('div'); - await page.waitForSelector( - 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div' - ); + await page.waitForTimeout(1000); + await page.locator('mapml-viewer').click({ position: { x: 250, y: 250 } }); + await page + .locator( + 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div' + ) + .waitFor(); const popupNum = await page.$eval( 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane', (div) => div.childElementCount @@ -126,32 +119,29 @@ test.describe('Multiple Extent Query Tests', () => { }); test('Only features from one extent are returned for queries inside its (non overlapping) bounds', async () => { - var numFeatures = await page.$eval( - 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > p', - (p) => p.innerText - ); - expect(numFeatures).toEqual('1/6'); - for (let i = 0; i < 6; i++) { - await page.click( - 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)' - ); - await page.waitForSelector( - 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > iframe' - ); - } - let feature = await page.$eval( - 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.mapml-vector-container > svg > g', - (g) => (g.firstElementChild ? g.firstElementChild : false) - ); - expect(feature).toBeFalsy(); - - const popup = await page.$eval( - 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > iframe', + await page.getByRole('button', { name: 'Close popup' }).click(); + const viewer = await page.locator('mapml-viewer'); + await viewer.evaluate((viewer) => { + viewer.reload(); + }); + // panning / zooming takes time... + await page.waitForTimeout(1000); + await page.locator('mapml-viewer').click({ position: { x: 450, y: 150 } }); + await page.getByTitle('Next Feature').click(); + await page.getByTitle('Next Feature').click(); + await page.getByTitle('Next Feature').click(); + await page.getByTitle('Next Feature').click(); + await page.getByTitle('Next Feature').click(); + let feature = page.locator('.mapml-vector-container > svg > g'); + await expect(feature).toBeEmpty(); + + const frame = page.locator('iframe'); + const popup = await frame.evaluate( (iframe) => iframe.contentWindow.document.querySelector('h1').innerText ); expect(popup).toEqual('No Geometry'); - numFeatures = await page.$eval( + let numFeatures = await page.$eval( 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > p', (p) => p.innerText ); @@ -159,14 +149,13 @@ test.describe('Multiple Extent Query Tests', () => { }); test('No features returned when queried outside of bounds of all extents', async () => { + await page.keyboard.press('Escape'); await page.evaluateHandle(() => document.querySelector('mapml-viewer').zoomTo(-18, 5, 0) ); - await page.click('div'); - await page.waitForSelector( - 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div', - { state: 'hidden' } - ); + // panning / zooming takes time... + await page.waitForTimeout(300); + await page.locator('mapml-viewer').click({ position: { x: 400, y: 250 } }); const popupNumRight = await page.$eval( 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane', (div) => div.childElementCount @@ -175,7 +164,8 @@ test.describe('Multiple Extent Query Tests', () => { await page.evaluateHandle(() => document.querySelector('mapml-viewer').zoomTo(-16, -40, 0) ); - await page.click('div'); + await page.waitForTimeout(300); + await page.locator('mapml-viewer').click({ position: { x: 250, y: 400 } }); const popupNumBottom = await page.$eval( 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane', (div) => div.childElementCount @@ -184,7 +174,8 @@ test.describe('Multiple Extent Query Tests', () => { await page.evaluateHandle(() => document.querySelector('mapml-viewer').zoomTo(33, -170, 0) ); - await page.click('div'); + await page.waitForTimeout(300); + await page.locator('mapml-viewer').click({ position: { x: 50, y: 250 } }); const popupNumLeft = await page.$eval( 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane', (div) => div.childElementCount @@ -193,7 +184,8 @@ test.describe('Multiple Extent Query Tests', () => { await page.evaluateHandle(() => document.querySelector('mapml-viewer').zoomTo(30, 98, 0) ); - await page.click('div'); + await page.waitForTimeout(300); + await page.locator('mapml-viewer').click({ position: { x: 250, y: 50 } }); const popupNumTop = await page.$eval( 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane', (div) => div.childElementCount diff --git a/test/e2e/layers/step/imageStep.test.js b/test/e2e/layers/step/imageStep.test.js index 590d3c338..4576ea5a3 100644 --- a/test/e2e/layers/step/imageStep.test.js +++ b/test/e2e/layers/step/imageStep.test.js @@ -9,7 +9,7 @@ test.describe('Templated image layer with step', () => { 1, 0, 0, - 'http://maps.geogratis.gc.ca/wms/toporama_en?SERVICE=WMS&REQUEST=GetMap&FORMAT=image/jpeg&TRANSPARENT=FALSE&STYLES=&VERSION=1.3.0&LAYERS=WMS-Toporama&WIDTH=300&HEIGHT=150&CRS=EPSG:3978&BBOX=', + '/images/toporama_en.jpg', '-5537023.0124460235,-2392385.4881043136,5972375.006350018,3362313.521293707&m4h=t', '', '-968982.6263652518,-107703.83540767431,1412272.136144273,1082923.545847088&m4h=t', diff --git a/test/e2e/layers/step/templatedImageLayerStep.html b/test/e2e/layers/step/templatedImageLayerStep.html index a25ca47ea..236419676 100644 --- a/test/e2e/layers/step/templatedImageLayerStep.html +++ b/test/e2e/layers/step/templatedImageLayerStep.html @@ -39,7 +39,7 @@ + tref="/images/toporama_en.jpg?SERVICE=WMS&REQUEST=GetMap&FORMAT=image/jpeg&TRANSPARENT=FALSE&STYLES=&VERSION=1.3.0&LAYERS=WMS-Toporama&WIDTH={w}&HEIGHT={h}&CRS=EPSG:3978&BBOX={xmin},{ymin},{xmax},{ymax}&m4h=t" >
diff --git a/test/e2e/layers/templatedImageLayer.html b/test/e2e/layers/templatedImageLayer.html index 5dbe7df21..ac709a1a7 100644 --- a/test/e2e/layers/templatedImageLayer.html +++ b/test/e2e/layers/templatedImageLayer.html @@ -42,7 +42,7 @@ + tref="images/toporama_en.jpg?WIDTH={w}&HEIGHT={h}&CRS=EPSG:3978&BBOX={xmin},{ymin},{xmax},{ymax}&m4h=t" > diff --git a/test/e2e/mapml-viewer/customTCRS.test.js b/test/e2e/mapml-viewer/customTCRS.test.js index 731e68a92..390d433e2 100644 --- a/test/e2e/mapml-viewer/customTCRS.test.js +++ b/test/e2e/mapml-viewer/customTCRS.test.js @@ -17,18 +17,25 @@ test.describe('Playwright Custom TCRS Tests', () => { }); test('Simple Custom TCRS, tiles load, mismatched layer disabled', async () => { + await page.waitForTimeout(100); const misMatchedLayerDisabled = await page.$eval( - 'body > mapml-viewer:nth-child(1) > layer-:nth-child(1)', - (layer) => layer.hasAttribute('disabled') + 'body > mapml-viewer:nth-child(1)', + (map) => map.querySelectorAll('layer-')[0].hasAttribute('disabled') + ); + + const matchedLayerEnabled = await page.$eval( + 'body > mapml-viewer:nth-child(1)', + (map) => map.querySelectorAll('layer-')[1].hasAttribute('disabled') ); const tilesLoaded = await page.$eval( - 'xpath=//html/body/mapml-viewer[1] >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(2) > div.leaflet-layer.mapml-static-tile-layer > div', + 'body > mapml-viewer:nth-child(1) >> .mapml-static-tile-layer > div', (tileGroup) => tileGroup.getElementsByTagName('map-tile').length ); expect(tilesLoaded).toEqual(2); expect(misMatchedLayerDisabled).toEqual(true); + expect(matchedLayerEnabled).toEqual(false); }); test('A projection name containing a colon is invalid', async () => { const message = await page.$eval( @@ -39,13 +46,13 @@ test.describe('Playwright Custom TCRS Tests', () => { }); test('Complex Custom TCRS, static features loaded, templated features loaded', async () => { const staticFeatures = await page.$eval( - 'body > mapml-viewer:nth-child(3) > layer-:nth-child(1)', - (layer) => layer.hasAttribute('disabled') + 'body > mapml-viewer:nth-child(3)', + (map) => map.querySelectorAll('layer-')[0].hasAttribute('disabled') ); const templatedFeatures = await page.$eval( - 'body > mapml-viewer:nth-child(3) > layer-:nth-child(2)', - (layer) => layer.hasAttribute('disabled') + 'body > mapml-viewer:nth-child(3)', + (map) => map.querySelectorAll('layer-')[1].hasAttribute('disabled') ); const featureOne = await page.$eval( diff --git a/test/e2e/mapml-viewer/locateButton.test.js b/test/e2e/mapml-viewer/locateButton.test.js index 6d7a8c0e6..f541f3225 100644 --- a/test/e2e/mapml-viewer/locateButton.test.js +++ b/test/e2e/mapml-viewer/locateButton.test.js @@ -22,14 +22,7 @@ test.describe('Geolocation control tests', () => { test('Using geolocation control to control map', async () => { await page.click('body > mapml-viewer'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Enter'); + await page.getByTitle('Show my location - location tracking off').click(); let locateButton_lat = await page.$eval( 'body > mapml-viewer', @@ -54,14 +47,7 @@ test.describe('Geolocation control tests', () => { test('Geolocation control state changes when pressed', async () => { await page.click('body > mapml-viewer'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Enter'); + await page.getByTitle('Show my location - location tracking on').click(); let locationOnText = await page.evaluate( () => M.options.locale.btnLocTrackOn @@ -79,7 +65,7 @@ test.describe('Geolocation control tests', () => { ); expect(locateButton_title1).toEqual(locationOffText); - await page.keyboard.press('Enter'); + await page.getByTitle('Show my location - location tracking off').click(); let locateButton_title2 = await page.$eval( 'div > div.leaflet-control-container > div.leaflet-bottom.leaflet-right > div > a', diff --git a/test/e2e/mapml-viewer/mapml-viewer.html b/test/e2e/mapml-viewer/mapml-viewer.html index 3e1300ec8..7e3dacf01 100644 --- a/test/e2e/mapml-viewer/mapml-viewer.html +++ b/test/e2e/mapml-viewer/mapml-viewer.html @@ -27,8 +27,8 @@ - - + + diff --git a/test/e2e/mapml-viewer/mapml-viewer.test.js b/test/e2e/mapml-viewer/mapml-viewer.test.js index 7c2b40f39..351aac29f 100644 --- a/test/e2e/mapml-viewer/mapml-viewer.test.js +++ b/test/e2e/mapml-viewer/mapml-viewer.test.js @@ -120,12 +120,9 @@ test.describe('Playwright mapml-viewer Element Tests', () => { await page.$eval('body > mapml-viewer', (layer) => layer.setAttribute('controlslist', 'nolayer') ); + let layerControl = await page.locator('.leaflet-control-layers'); + await expect(layerControl).toBeHidden(); - let layerControlHidden = await page.$eval( - '.leaflet-top.leaflet-right', - (div) => div.firstChild.hidden - ); - expect(layerControlHidden).toEqual(true); await page.click('body > mapml-viewer', { button: 'right' }); // toggle controls await page.click('.mapml-contextmenu > button:nth-of-type(6)'); @@ -133,11 +130,7 @@ test.describe('Playwright mapml-viewer Element Tests', () => { // toggle controls await page.click('.mapml-contextmenu > button:nth-of-type(6)'); - layerControlHidden = await page.$eval( - '.leaflet-top.leaflet-right', - (div) => div.firstChild.hidden - ); - expect(layerControlHidden).toEqual(true); + await expect(layerControl).toBeHidden(); }); }); }); @@ -184,13 +177,16 @@ test.describe('Playwright mapml-viewer Element Tests', () => { }); test('Paste Invalid link to map using ctrl+v', async () => { + await page.pause(); await page.click('body > textarea#invalidLink'); await page.keyboard.press('Control+a'); await page.keyboard.press('Control+c'); await page.click('body > mapml-viewer'); await page.keyboard.press('Control+v'); - await page.waitForTimeout(1000); + await page.$eval('body > mapml-viewer', (viewer) => + viewer.whenLayersReady() + ); const layerCount = await page.$eval( 'body > mapml-viewer', (map) => map.layers.length diff --git a/test/e2e/mapml-viewer/viewerContextMenu.test.js b/test/e2e/mapml-viewer/viewerContextMenu.test.js index 6cef96fc0..a6b616c30 100644 --- a/test/e2e/mapml-viewer/viewerContextMenu.test.js +++ b/test/e2e/mapml-viewer/viewerContextMenu.test.js @@ -342,8 +342,11 @@ test.describe('Playwright mapml-viewer Context Menu (and api) Tests', () => { test('Submenu, copy map (MapML)', async () => { await page.reload(); - await page.waitForTimeout(3000); - await page.click('body > mapml-viewer'); + const viewer = page.getByTestId('testviewer'); + // have to wait for whenLayersReady because the extent sprouts implicit attributes + // from properties that are set by default + await viewer.evaluate((viewer) => viewer.whenLayersReady()); + await viewer.click(); await page.keyboard.press('Shift+F10'); await page.keyboard.press('Tab'); await page.keyboard.press('Enter'); @@ -355,9 +358,9 @@ test.describe('Playwright mapml-viewer Context Menu (and api) Tests', () => { 'body > textarea#coord', (text) => text.value ); - const expected = ` - -