From 12a6ee25b5fdc01ce3cc1c9c6200fabcbed2ddc5 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Tue, 7 Nov 2023 09:55:30 -0500 Subject: [PATCH] Create map-extent, map-input, map-link (#887) * initial map-input setup * implement Classes for different types of map-input types * Update map-input implementation + Add support for tilematrix type=locataion Class * initial map-link setup * Make map-input.connectedCallback async, await on extent.whenReady() for setup * Update version to 0.12.0 in package.json * migrating extent initialization from MapMLLayer to map-extent.js * updating and adding and removing layer control extent HTML current problem is that this._layerControlHTML is not connected when disconnecting the map-extent from the DOM (manually), so unable to hide/unhide the extents' root fieldset line 620 map-extent.js * map-extent hidden API * map-extent Get checkbox working via API map-extent.checked. Change TemplatedLayer and related child layer types. * Fix TemplatedFeaturesLayer checking/unchecking behaviour. Prevent excessive disconnection / reconnection of layers and map-extents when clicking on layer control entries, by upping the 'moving' px threshold and calculating if the entry has changed position through the dragging event (do disconnect/reconnect) or not (do not do dis/re). In map-feature, look for data-moving on layer or map-extent element, regardless of whether it's a local or remote layer (light dom vs shadow dom). * Set 'Sub-layer' as default label attribute, add to options messages Synchronize opacity attribute, property and slider in layer control (note: works slightly differently than layer.opacity, which doesn't "sprout" the opacity attribute when set via layer control) Fix bug when setting map-extent.hidden in shadow dom of layer element * Move/ refactor MapMLLayer._setLayerElExtent into layer.js layer-.extent /layer-._calculateExtent(). To Do: make it efficient / memoize extent value until extent actually changes. * Fix bug in _calculateExtent, add whenReady() to layer.zoomTo * get tests configured with new map-extent attributes. Get rid of (some) use of live map tiles from NRCan. Add temporary todo list of still-failing tests * Fix a test done with map-extent hidden attribute implemented via CE * Make opacity work the same in map-extent as it does in layer. This is the documented behaviour too. * Make layer-.extent (layer-._calculateExtent()) memoized * Get map-extent label API working * fix/refactor query map-extents * avoid remote map-feature (in mapml) triggering MapFeature connectedCallback twice when it is parsed in form of text/html * Fix remote queryable layer with hidden extent not queryable. * Re-write AnnounceMovement.js totalBounds event handler to use layer-.extent. Create extent.getBounds() function which may be better written as a util function, since it's Leaflet specific. * create layer-.whenElemsReady method to check if the children elems of layer- are ready, create Util.extentToBounds for announcemovement * refactor debugMode, totalLayerBounds, AnnounceMovement * Get debugMode.test.js working * featureIndexOverlayResults needs to be in slowMo to work reliably WIP: Add data-testid to multipleExtents.test fixture. Update playwright to use locators, locator assertions. Note that I don't believe the layer bounds were ever being rendered by the debugOverlay, only a duplicate of the map-link at the map-extent level, which seemed to be that of the layer-, but was only a duplicate due to misuse of layerBounds at the map-extent level. To be discussed with team. Note that current failing test fails due to the map extent in the layer control not being correctly disabled, even though (I believe) the is disabled (has disabled attribute, at least). * WIP on multiple extents test semantics in-progress elimination of MapMLLayer._properties._mapExtents. * Refactor layer.js._calculateExtent into layer.js.extent + MapMLLayer._calculateBounds + map-extent.js._calculateBounds Fix problem with MapMLLayer._calculateExtent that wasn't calculating the zoom bounds properly. Problem visible on main when clicking on Finland or Sweden in Arctic SDI. Add some guard conditions to AnnounceMovement.totalBounds, which was firing too early in the map's life cycle and was causing issues with layers and descendant elements being not ready. Make layer.whenReady() a bit stronger, by determining if the shadowroot is ready too, which may head off some timing problems. * WIP on multiple extents test semantics. * get rid of layerControl._validateInput, make layer- and map-extent elements create and update the layerControl by themselves when needed; move the logic of creating layer-'s layerControlHTML from MapMLLayer.js to layer-.js * Upgrade playwright to 1.39.0 Update multipleExtents.test.js * WIP: refactor to get rid of _properties._mapExtents, make layer- and map-extent handle their own layerControlHTML (create and update), fix bugs showing up in experiments * add event listener on layer- to listen to layer- checked property changes, when it is changed trigger the _handlechange method of mapExtent to ensure the mapExtent._templatedLayer can be added to / removed from the map properly * remove trailing '/' from from xmlns links * Init checkbox defaultChecked for map-extent, layer- * get rid of MapMLLayer._validateExtent, add projection check in mapExtent._validateDisabled, create .catch for use cases of mapExtent.whenReady(), handle the timing issue of projection change * clean up the templatedTileLayer._container when the templatedTileLayer is removed, to fix the bug that the templated tiles cannot be rendered properly when zoom level is changed before it is readded to the map * Add rejection to map-extent, map-input in the case of the element being disconnected while waiting for whenReady. This can happen during API usage for example, when changing the map projection soon after map creation, resulting in the connectedCallbacks being called and attributeChangedCallbacks being called but the element is disconnected before that cycle can complete. Added the rejection in this case so that it is clear what happens. Added conditional enabling of announceMovement handler after projection change on mapml-viewer, because it should only be enabled when the setting calls for it to be so (otherwise errors can occur during the totalBounds method of the handler, due to nothing being ready). * Make it so that createLayerControlHTML works unconditionally, was causing issues when the layer was re-initialized due to changeprojection event happening during initialization. * Enable as a control in , which will break keyboard tests, possibly others, because tab order to get to the select will be different. WIP / to do: disable the extent controls, italicize the text labels for them * get layer and extent layer control disabled properly + move over code for map-extent.createLayerControlExtentHTML to a seperate supporting file * update Gruntfile * grouping the radio buttons of styles in layerControl based on an unique identifier, to fix the bug that style does not have a default selection * invoke mapExtent.handlechange when the map projection is changed to make sure templatedLayers are properly added to the map; remove the map-change event listener of layer- after the map-extent is disconnected * Workaround leaflet bug when changing map.options.crs #2553 * Workaround leaflet bug when changing map.options.crs #2553 part deux * Make opacity implementation the same for both layer- and map-extent Fix incorrect reference to _propertiesGroupAnatomy for map-extent (it's a property of parent layer element), it's the root fieldset that contains the individual map-extent fieldsets. Change class on map-extent's
for opacity from `mapml-layer-item-details` to `mapml-layer-item-opacity mapml-control-layers` Fix checked attribute for layer test failure * add the setTimeout for the layer-._validateDisable back, to make sure the validateDisabled happen after the moveend so that the result for validation is correct * Sync mapml-viewer -> web-map.js * Enable layer-.whenReady to handle fetch errors without waiting for the full 5s timeout. * Get tests working * Remove .only from some tests preventing full suite from running * sync multipleExtents tests with PR * Fix markup in mapSpan.html * fix failing test; migrate map-meta[name] from remote mapml to the shadowroot of layer-, make the staticTileLayer read zoom bounds from map-meta instead of map-tile, add whenReady in MapInput.attributechangecallback * fix the typo from map-meta[name=projection] to map-meta[name=zoom]; do L.extend on this.options instead of on the local options var * add settimeout in debugoverlay._addbounds to delay the function call until the layer.layerbounds / layer.extentbounds are ready after layer-.checked attribute is changed, modify the test to adapt the changes * sync mapml-viewer test with web-map * add constants.js to dist and import it in map-extent.js, remove unused import for constant.js and some unused variables in other files * Projection change test when following link to layer with default bounds based on the name, ('Debug components update with projection changes') this test may need revisiting; the only thing that is tested is that the map bounds changes when the link is loaded to replace the current layer. To me it seems that the test is failing silently on main tbd * make the minzoom and maxzoom of staticTileLayer read from map-meta instead of being calculated the map-tile zoom attribute; prettier formatting for projectionchange.test; enable ci testing * Add whenReady().then().catch() blocks to layer-.attributeChangedCallback usage. Lack of catch processing led to slow processing when pasting an invalid / unavailable link onto the map. * Set --workers=1 for playwright testing, be kind to github actions * add slowmo to the debugmode.test, history.test * add slowmo to a few tests to get GitHub CI testing pass * change the returned format of the zoom getter of mapmlviewer from String to Number, update tests accordingly; some prettier formatting for tms and clientTemplatedTileLayer tests * change map lat, lon, width and height getters to return a Number instead of String, update tests * add waitfortimeout for flaky tests to pass GitHub CI testing * Return number for layer-, map-extent.opacity, mapml-viewer.width, .height, .lat, .lon. Amend tests to enforce this. * move dist to right place for grunt web-map-doc * Bundle everything possible into mapml.js, use M. to access global symbols * initialize this._opacity of layer- and map-extent with the attribute value (if exists) or 1.0 as default; add waitfortimeout and slowmo to fix flaky tests * Add tests for map-extent attribute api. Fix bug in same. * Add test for template feature being disabled when going out of bounds * Change class on opacity details/summary for map-extent layer control --------- Co-authored-by: AliyanH Co-authored-by: prushfor Co-authored-by: Aliyan Haq <55751566+AliyanH@users.noreply.github.com> Co-authored-by: yhy0217 --- Gruntfile.js | 5 +- index.html | 114 +- package-lock.json | 63 +- package.json | 8 +- src/layer.js | 229 +-- src/map-extent.js | 575 ++++++- src/map-feature.js | 95 +- src/map-input.js | 292 +++- src/map-link.js | 27 +- src/mapml-viewer.js | 41 +- src/mapml.css | 10 +- src/mapml/control/LayerControl.js | 57 +- .../extents/createLayerControlForExtent.js | 282 ++++ .../elementSupport/inputs/heightInput.js | 18 + .../elementSupport/inputs/hiddenInput.js | 19 + .../elementSupport/inputs/locationInput.js | 116 ++ src/mapml/elementSupport/inputs/widthInput.js | 18 + src/mapml/elementSupport/inputs/zoomInput.js | 26 + .../layers/createLayerControlForLayer.js | 305 ++++ src/mapml/handlers/AnnounceMovement.js | 31 +- src/mapml/index.js | 28 + src/mapml/layers/DebugOverlay.js | 140 +- src/mapml/layers/FeatureLayer.js | 2 - src/mapml/layers/MapMLLayer.js | 1353 ++--------------- src/mapml/layers/StaticTileLayer.js | 22 +- src/mapml/layers/TemplatedFeaturesLayer.js | 22 +- src/mapml/layers/TemplatedImageLayer.js | 5 +- src/mapml/layers/TemplatedLayer.js | 10 +- src/mapml/layers/TemplatedTileLayer.js | 16 +- src/mapml/options.js | 1 + src/mapml/utils/Constants.js | 4 - src/mapml/utils/DOMTokenList.js | 2 +- src/mapml/utils/Util.js | 51 +- src/web-map.js | 45 +- test/e2e/api/domApi-mapml-viewer.test.js | 13 +- test/e2e/api/domApi-web-map.test.js | 12 +- test/e2e/api/locateApi.html | 2 +- test/e2e/core/ArrowKeyNavContextMenu.html | 2 +- test/e2e/core/debugMode.html | 14 +- test/e2e/core/debugMode.test.js | 52 +- test/e2e/core/dragDrop.test.js | 4 - test/e2e/core/dragDropMap.test.js | 4 - test/e2e/core/featureIndexOverlay.html | 4 +- .../core/featureIndexOverlayResults.test.js | 2 +- test/e2e/core/featureNavigation.html | 2 +- .../core/fullscreenControlMapmlViewer.html | 4 +- test/e2e/core/fullscreenControlWebMap.html | 4 +- test/e2e/core/history.test.js | 2 +- test/e2e/core/layerAttributes.test.js | 4 +- test/e2e/core/layerContextMenu.html | 2 +- test/e2e/core/layerContextMenu.test.js | 2 +- test/e2e/core/linkTypes.test.js | 2 +- test/e2e/core/mapContextMenu.html | 2 +- test/e2e/core/mapContextMenu.test.js | 2 +- test/e2e/core/mapElement.html | 2 +- test/e2e/core/mapElement.test.js | 1 + test/e2e/core/mapSpan.html | 28 +- test/e2e/core/metaDefault.html | 2 +- test/e2e/core/metaDefault.test.js | 30 +- test/e2e/core/missingMetaParameters.html | 4 +- test/e2e/core/popupTabNavigation.html | 2 +- test/e2e/core/projectionChange.html | 4 +- test/e2e/core/projectionChange.test.js | 185 +-- test/e2e/core/reticle.html | 2 +- test/e2e/core/styleParsing.html | 4 +- test/e2e/core/styleParsing.test.js | 1 + test/e2e/core/tms.html | 2 +- test/e2e/core/tms.test.js | 2 +- test/e2e/data/cbmt.mapml | 4 +- test/e2e/data/cbmtgeom.mapml | 4 +- test/e2e/data/cbmtile-cbmt.mapml | 4 +- test/e2e/data/fdi.mapml | 4 +- test/e2e/data/osm.mapml | 4 +- test/e2e/data/tiles/cbmt/DouglasFir.mapml | 2 +- .../tiles/cbmt/cbmt-changeProjection.mapml | 2 +- test/e2e/data/tiles/cbmt/cbmt.mapml | 2 +- .../e2e/data/tiles/cbmt/missing_min_max.mapml | 2 +- .../tiles/cbmt/osm-changeProjection.mapml | 2 +- test/e2e/data/tiles/cbmt/templatedImage.mapml | 2 +- .../data/tiles/wgs84/vector-tile-test.mapml | 2 +- test/e2e/elements/map-extent/map-extent.html | 65 + .../elements/map-extent/map-extent.test.js | 157 ++ test/e2e/features/mapFeature.test.js | 18 +- .../e2e/layers/CustomProjectionLayers.test.js | 2 +- test/e2e/layers/clientTemplatedTileLayer.html | 2 +- .../layers/clientTemplatedTileLayer.test.js | 2 +- test/e2e/layers/featureLayer.test.js | 2 +- test/e2e/layers/general/isVisible.js | 16 +- test/e2e/layers/layerControl.test.js | 4 +- test/e2e/layers/layerOpacityAttribute.html | 2 +- test/e2e/layers/layerOpacityAttribute.test.js | 6 +- test/e2e/layers/multipleExtents.html | 19 +- test/e2e/layers/multipleExtents.test.js | 271 ++-- test/e2e/layers/multipleExtentsOpacity.html | 6 +- .../e2e/layers/multipleExtentsOpacity.test.js | 29 +- .../multipleHeterogeneousQueryExtents.html | 6 +- test/e2e/layers/multipleQueryExtents.html | 8 +- test/e2e/layers/queryLink.html | 2 +- test/e2e/layers/queryLink.test.js | 4 +- test/e2e/layers/queryableMapExtent.mapml | 2 +- test/e2e/layers/queryableMapExtent.test.js | 2 +- .../step/templatedFeaturesLayerStep.html | 2 +- .../layers/step/templatedImageLayerStep.html | 2 +- .../layers/step/templatedTileLayerStep.html | 2 +- test/e2e/layers/templatedFeatures.html | 8 +- test/e2e/layers/templatedFeatures.test.js | 24 +- test/e2e/layers/templatedFeaturesFilter.html | 2 +- .../layers/templatedFeaturesFilter.test.js | 5 +- test/e2e/layers/templatedImageLayer.html | 2 +- test/e2e/layers/templatedTileLayer.html | 7 +- test/e2e/layers/templatedTileLayer.test.js | 9 +- test/e2e/mapml-viewer/cssDomination.html | 2 +- test/e2e/mapml-viewer/cssDomination.test.js | 4 +- test/e2e/mapml-viewer/customTCRS.html | 4 +- test/e2e/mapml-viewer/customTCRS.test.js | 9 +- test/e2e/mapml-viewer/locateButton.html | 2 +- test/e2e/mapml-viewer/mapml-viewer.html | 2 +- test/e2e/mapml-viewer/mapml-viewer.test.js | 6 +- .../e2e/mapml-viewer/mapml-viewerCaption.html | 2 +- .../mapml-viewerHeightAndWidthAttributes.html | 2 +- ...pml-viewerHeightAndWidthAttributes.test.js | 4 +- test/e2e/mapml-viewer/noWidthAndHeight.html | 2 +- test/e2e/mapml-viewer/staticAttribute.html | 2 +- .../mapml-viewer/viewerContextMenu.test.js | 2 +- test/e2e/mapml-viewer/windowSizeChange.html | 2 +- test/e2e/web-map/map.html | 2 +- test/e2e/web-map/map.test.js | 2 +- test/e2e/web-map/mapCaption.html | 2 +- test/e2e/web-map/mapCssNoDomination.html | 2 +- test/e2e/web-map/mapCssNoDomination.test.js | 4 +- .../web-map/mapHeightAndWidthAttributes.html | 2 +- .../mapHeightAndWidthAttributes.test.js | 4 +- test/e2e/web-map/mapNoWidthAndHeight.html | 2 +- test/e2e/web-map/mapStatic.html | 2 +- test/e2e/web-map/mapWindowSizeChange.html | 2 +- test/server.js | 1 + 136 files changed, 2988 insertions(+), 2240 deletions(-) create mode 100644 src/mapml/elementSupport/extents/createLayerControlForExtent.js create mode 100644 src/mapml/elementSupport/inputs/heightInput.js create mode 100644 src/mapml/elementSupport/inputs/hiddenInput.js create mode 100644 src/mapml/elementSupport/inputs/locationInput.js create mode 100644 src/mapml/elementSupport/inputs/widthInput.js create mode 100644 src/mapml/elementSupport/inputs/zoomInput.js create mode 100644 src/mapml/elementSupport/layers/createLayerControlForLayer.js delete mode 100644 src/mapml/utils/Constants.js create mode 100644 test/e2e/elements/map-extent/map-extent.html create mode 100644 test/e2e/elements/map-extent/map-extent.test.js diff --git a/Gruntfile.js b/Gruntfile.js index 41145842b..c7c25443c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -25,10 +25,11 @@ module.exports = function(grunt) { 'dist/mapml.js': ['<%= rollup.main.dest %>'], 'dist/web-map.js': ['src/web-map.js'], 'dist/mapml-viewer.js': ['src/mapml-viewer.js'], - 'dist/DOMTokenList.js': ['src/mapml/utils/DOMTokenList.js'], 'dist/map-caption.js': ['src/map-caption.js'], 'dist/map-feature.js': ['src/map-feature.js'], 'dist/map-extent.js': ['src/map-extent.js'], + 'dist/map-input.js': ['src/map-input.js'], + 'dist/map-link.js': ['src/map-link.js'], 'dist/map-area.js': ['src/map-area.js'], 'dist/layer.js': ['src/layer.js'], 'dist/leaflet.js': ['dist/leaflet-src.js', @@ -159,7 +160,7 @@ module.exports = function(grunt) { { expand: true, src: ['dist/*'], - dest: '../web-map-doc' + dest: '../web-map-doc/static' } ] } diff --git a/index.html b/index.html index a1919378b..7040693c5 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@ /* Responsive map. */ max-width: 100%; - /* Full viewport. */ + /* Full viewport. */ width: 100%; height: 100%; @@ -72,56 +72,64 @@ - - - - - - - - Click me! - - - - - -75.8242035 45.3526278 -75.6793213 45.4572409 -75.5680847 45.4692806 -75.6092834 45.4215881 -75.5756378 45.3810901 -75.7946777 45.3120804 - - - - - - - - - - - - Point 1 - - - -75.6978285 45.4202251 - - - - - - Point 2 - - - -75.7002854 45.4199465 - - - - - - point 3 - - - -75.6984723 45.4179207 - - - - - - + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 7b89c3847..4ae3721dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@maps4html/web-map-custom-element", - "version": "0.11.0", + "version": "0.12.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@maps4html/web-map-custom-element", - "version": "0.11.0", + "version": "0.12.0", "license": "W3C", "devDependencies": { - "@playwright/test": "^1.35.1", + "@playwright/test": "^1.39.0", "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.35.1", + "playwright": "^1.39.0", "proj4": "^2.6.2", "proj4leaflet": "^1.0.2", "rollup": "^2.23.1" @@ -1513,22 +1513,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", - "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", "dev": true, "dependencies": { - "@types/node": "*", - "playwright-core": "1.35.1" + "playwright": "1.39.0" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } }, "node_modules/@sideway/address": { @@ -9740,25 +9736,27 @@ } }, "node_modules/playwright": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.35.1.tgz", - "integrity": "sha512-NbwBeGJLu5m7VGM0+xtlmLAH9VUfWwYOhUi/lSEDyGg46r1CA9RWlvoc5yywxR9AzQb0mOCm7bWtOXV7/w43ZA==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", "dev": true, - "hasInstallScript": true, "dependencies": { - "playwright-core": "1.35.1" + "playwright-core": "1.39.0" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", - "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -13446,14 +13444,12 @@ } }, "@playwright/test": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", - "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", "dev": true, "requires": { - "@types/node": "*", - "fsevents": "2.3.2", - "playwright-core": "1.35.1" + "playwright": "1.39.0" } }, "@sideway/address": { @@ -19810,18 +19806,19 @@ } }, "playwright": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.35.1.tgz", - "integrity": "sha512-NbwBeGJLu5m7VGM0+xtlmLAH9VUfWwYOhUi/lSEDyGg46r1CA9RWlvoc5yywxR9AzQb0mOCm7bWtOXV7/w43ZA==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", "dev": true, "requires": { - "playwright-core": "1.35.1" + "fsevents": "2.3.2", + "playwright-core": "1.39.0" } }, "playwright-core": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", - "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", "dev": true }, "posix-character-classes": { diff --git a/package.json b/package.json index 717ff2fbf..eb9e9cdf2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@maps4html/web-map-custom-element", - "version": "0.11.0", + "version": "0.12.0", "description": "web-map customized built-in HTML or custom ", "keywords": [ "mapml-viewer", @@ -32,10 +32,11 @@ } ], "scripts": { - "test": "npx playwright test --retries=3", + "test": "npx playwright test --retries=3 --workers=1", "jest": "jest --verbose --noStackTrace" }, "devDependencies": { + "@playwright/test": "^1.39.0", "diff": "^5.1.0", "express": "^4.17.1", "grunt": "^1.4.0", @@ -56,8 +57,7 @@ "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.79.0", "path": "^0.12.7", - "@playwright/test": "^1.35.1", - "playwright": "^1.35.1", + "playwright": "^1.39.0", "proj4": "^2.6.2", "proj4leaflet": "^1.0.2", "rollup": "^2.23.1" diff --git a/src/layer.js b/src/layer.js index e792f090d..59c492d0a 100644 --- a/src/layer.js +++ b/src/layer.js @@ -49,7 +49,7 @@ export class MapLayer extends HTMLElement { get opacity() { // use ?? since 0 is falsy, || would return rhs in that case - return this._opacity ?? this.getAttribute('opacity'); + return +(this._opacity ?? this.getAttribute('opacity')); } set opacity(val) { @@ -57,18 +57,28 @@ export class MapLayer extends HTMLElement { this.setAttribute('opacity', val); } + get extent() { + // calculate the bounds of all content, return it. + if (!this._layer.bounds) { + this._layer._calculateBounds(); + } + return Object.assign( + M._convertAndFormatPCRS( + this._layer.bounds, + this._layer._properties.crs, + this._layer._properties.projection + ), + { zoom: this._layer.zoomBounds } + ); + } + constructor() { // Always call super first in constructor super(); } disconnectedCallback() { - // console.log('Custom map element removed from page.'); // if the map-layer node is removed from the dom, the layer should be // removed from the map and the layer control - - // this is moved up here so that the layer control doesn't respond - // to the layer being removed with the _onLayerChange execution - // that is set up in _attached: if (this.hasAttribute('data-moving')) return; this._onRemove(); } @@ -86,6 +96,7 @@ export class MapLayer extends HTMLElement { this._layerControl.removeLayer(this._layer); } delete this._layer; + delete this._fetchError; if (this.shadowRoot) { this.shadowRoot.innerHTML = ''; @@ -94,6 +105,10 @@ export class MapLayer extends HTMLElement { connectedCallback() { if (this.hasAttribute('data-moving')) return; + this._createLayerControlHTML = M._createLayerControlHTML.bind(this); + // this._opacity is used to record the current opacity value (with or without updates), + // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 + this._opacity = +(this.getAttribute('opacity') || 1.0); const doConnected = this._onAdd.bind(this); this.parentElement .whenReady() @@ -158,11 +173,13 @@ export class MapLayer extends HTMLElement { opacity: this.opacity } ); + this._createLayerControlHTML(); this._attachedToMap(); this._validateDisabled(); resolve(); }) .catch((error) => { + this._fetchError = true; console.log('Error fetching layer content' + error); }); } else { @@ -173,6 +190,7 @@ export class MapLayer extends HTMLElement { mapprojection: this.parentElement.projection, opacity: this.opacity }); + this._createLayerControlHTML(); this._attachedToMap(); this._validateDisabled(); resolve(); @@ -211,17 +229,11 @@ export class MapLayer extends HTMLElement { }); // 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 @@ -260,19 +272,28 @@ export class MapLayer extends HTMLElement { attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'label': - this.whenReady().then(() => { - this._layer.setName(newValue); - }); + this.whenReady() + .then(() => { + this._layer.setName(newValue); + }) + .catch((e) => { + console.log(e); + }); break; case 'checked': - if (this._layer) { - if (typeof newValue === 'string') { - this.parentElement._map.addLayer(this._layer); - } else { - this.parentElement._map.removeLayer(this._layer); - } - this.dispatchEvent(new Event('change', { bubbles: true })); - } + this.whenReady() + .then(() => { + if (typeof newValue === 'string') { + this.parentElement._map.addLayer(this._layer); + } else { + this.parentElement._map.removeLayer(this._layer); + } + this._layerControlCheckbox.checked = this.checked; + this.dispatchEvent(new CustomEvent('map-change')); + }) + .catch((e) => { + console.log(e); + }); break; case 'hidden': var map = this.parentElement && this.parentElement._map; @@ -306,12 +327,18 @@ export class MapLayer extends HTMLElement { } } _validateDisabled() { + // setTimeout is necessary to make the validateDisabled happen later than the moveend operations etc., + // to ensure that the validated result is correct setTimeout(() => { let layer = this._layer, map = layer?._map; if (map) { - let count = 0, - total = 0, + // prerequisite: no inline and remote mapml elements exists at the same time + const mapExtents = this.shadowRoot + ? this.shadowRoot.querySelectorAll('map-extent') + : this.querySelectorAll('map-extent'); + let disabledExtentCount = 0, + totalExtentCount = 0, layerTypes = [ '_staticTileLayer', '_imageLayer', @@ -321,62 +348,71 @@ export class MapLayer extends HTMLElement { if (layer.validProjection) { for (let j = 0; j < layerTypes.length; j++) { let type = layerTypes[j]; - if (this.checked && layer[type]) { - if (type === '_templatedLayer') { - for (let i = 0; i < layer._properties._mapExtents.length; i++) { - for ( - let j = 0; - j < - layer._properties._mapExtents[i].templatedLayer._templates - .length; - j++ - ) { - if ( - layer._properties._mapExtents[i].templatedLayer - ._templates[j].rel === 'query' - ) - continue; - total++; - layer._properties._mapExtents[i].removeAttribute( - 'disabled' - ); - layer._properties._mapExtents[i].disabled = false; - if ( - !layer._properties._mapExtents[i].templatedLayer - ._templates[j].layer.isVisible - ) { - count++; - layer._properties._mapExtents[i].setAttribute( - 'disabled', - '' - ); - layer._properties._mapExtents[i].disabled = true; - } - } + if (this.checked) { + if (type === '_templatedLayer' && mapExtents.length > 0) { + for (let i = 0; i < mapExtents.length; i++) { + totalExtentCount++; + if (mapExtents[i]._validateDisabled()) disabledExtentCount++; } - } else { - total++; - if (!layer[type].isVisible) count++; + } else if (layer[type]) { + // not a templated layer + totalExtentCount++; + if (!layer[type].isVisible) disabledExtentCount++; } } } } else { - count = 1; - total = 1; + disabledExtentCount = 1; + totalExtentCount = 1; } - - if (count === total && count !== 0) { - this.setAttribute('disabled', ''); //set a disabled attribute on the layer element + // if all extents are not visible / disabled, set layer to disabled + if ( + disabledExtentCount === totalExtentCount && + disabledExtentCount !== 0 + ) { + this.setAttribute('disabled', ''); this.disabled = true; } else { - //might be better not to disable the layer controls, might want to deselect layer even when its out of bounds this.removeAttribute('disabled'); this.disabled = false; } - map.fire('validate'); + this.toggleLayerControlDisabled(); } }, 0); } + + // disable/italicize layer control elements based on the layer-.disabled property + toggleLayerControlDisabled() { + let input = this._layerControlCheckbox, + label = this._layerControlLabel, + opacityControl = this._opacityControl, + opacitySlider = this._opacitySlider, + styleControl = this._styles; + if (this.disabled) { + input.disabled = true; + opacitySlider.disabled = true; + label.style.fontStyle = 'italic'; + opacityControl.style.fontStyle = 'italic'; + if (styleControl) { + styleControl.style.fontStyle = 'italic'; + styleControl.querySelectorAll('input').forEach((i) => { + i.disabled = true; + }); + } + } else { + input.disabled = false; + opacitySlider.disabled = false; + label.style.fontStyle = 'normal'; + opacityControl.style.fontStyle = 'normal'; + if (styleControl) { + styleControl.style.fontStyle = 'normal'; + styleControl.querySelectorAll('input').forEach((i) => { + i.disabled = false; + }); + } + } + } + getOuterHTML() { let tempElement = this.cloneNode(true); @@ -419,29 +455,23 @@ export class MapLayer extends HTMLElement { return outerLayer; } - _onLayerChange() { - if (this._layer._map) { - // can't disable observers, have to set a flag telling it where - // the 'event' comes from: either the api or a user click/tap - // may not be necessary -> this._apiToggleChecked = false; - this.checked = this._layer._map.hasLayer(this._layer); - } - } zoomTo() { - if (!this.extent) return; - let map = this.parentElement._map, - tL = this.extent.topLeft.pcrs, - bR = this.extent.bottomRight.pcrs, - layerBounds = L.bounds( - L.point(tL.horizontal, tL.vertical), - L.point(bR.horizontal, bR.vertical) - ), - center = map.options.crs.unproject(layerBounds.getCenter(true)); + this.whenElemsReady().then(() => { + let map = this.parentElement._map, + extent = this.extent, + tL = extent.topLeft.pcrs, + bR = extent.bottomRight.pcrs, + layerBounds = L.bounds( + L.point(tL.horizontal, tL.vertical), + L.point(bR.horizontal, bR.vertical) + ), + center = map.options.crs.unproject(layerBounds.getCenter(true)); - let maxZoom = this.extent.zoom.maxZoom, - minZoom = this.extent.zoom.minZoom; - map.setView(center, M.getMaxZoom(layerBounds, map, minZoom, maxZoom), { - animate: false + let maxZoom = extent.zoom.maxZoom, + minZoom = extent.zoom.minZoom; + map.setView(center, M.getMaxZoom(layerBounds, map, minZoom, maxZoom), { + animate: false + }); }); } mapml2geojson(options = {}) { @@ -467,7 +497,7 @@ export class MapLayer extends HTMLElement { whenReady() { return new Promise((resolve, reject) => { let interval, failureTimer; - if (this._layer) { + if (this._layer && (!this.src || this.shadowRoot?.childNodes.length)) { resolve(); } else { let layerElement = this; @@ -475,10 +505,17 @@ export class MapLayer extends HTMLElement { failureTimer = setTimeout(layerNotDefined, 5000); } function testForLayer(layerElement) { - if (layerElement._layer) { + if ( + layerElement._layer && + (!layerElement.src || layerElement.shadowRoot?.childNodes.length) + ) { clearInterval(interval); clearTimeout(failureTimer); resolve(); + } else if (layerElement._fetchError) { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Error fetching layer content'); } } function layerNotDefined() { @@ -488,4 +525,16 @@ export class MapLayer extends HTMLElement { } }); } + // check if all child elements are ready + whenElemsReady() { + let elemsReady = []; + let target = this.shadowRoot || this; + for (let elem of [ + ...target.querySelectorAll('map-extent'), + ...target.querySelectorAll('map-feature') + ]) { + elemsReady.push(elem.whenReady()); + } + return Promise.allSettled(elemsReady); + } } diff --git a/src/map-extent.js b/src/map-extent.js index 5808586a5..2fbacd51a 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -1,6 +1,7 @@ +/* global M */ export class MapExtent extends HTMLElement { static get observedAttributes() { - return ['units', 'checked', 'label', 'opacity']; + return ['units', 'checked', 'label', 'opacity', 'hidden']; } get units() { return this.getAttribute('units'); @@ -24,7 +25,9 @@ export class MapExtent extends HTMLElement { } } get label() { - return this.hasAttribute('label') ? this.getAttribute('label') : ''; + return this.hasAttribute('label') + ? this.getAttribute('label') + : M.options.locale.dfExtent; } set label(val) { if (val) { @@ -32,66 +35,554 @@ export class MapExtent extends HTMLElement { } } get opacity() { - return this._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.setAttribute('opacity', val); } + get hidden() { + return this.hasAttribute('hidden'); + } + + set hidden(val) { + if (val) { + this.setAttribute('hidden', ''); + } else { + this.removeAttribute('hidden'); + } + } attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case 'units': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'label': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'checked': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'opacity': - if (oldValue !== newValue) { - // handle side effects + this.whenReady() + .then(() => { + switch (name) { + case 'units': + if (oldValue !== newValue) { + // handle side effects + } + break; + case 'label': + if (oldValue !== newValue) { + this._layerControlHTML.querySelector( + '.mapml-layer-item-name' + ).innerHTML = newValue || M.options.locale.dfExtent; + } + break; + case 'checked': + this._handleChange(); + this._calculateBounds(); + this._layerControlCheckbox.checked = newValue !== null; + break; + case 'opacity': + if (oldValue !== newValue) { + this._opacity = newValue; + if (this._templatedLayer) + this._templatedLayer.changeOpacity(newValue); + } + break; + case 'hidden': + if (oldValue !== newValue) { + let extentsRootFieldset = + this.parentLayer._propertiesGroupAnatomy; + let position = Array.from( + this.parentNode.querySelectorAll('map-extent:not([hidden])') + ).indexOf(this); + if (newValue !== null) { + // remove from layer control (hide from user) + this._layerControlHTML.remove(); + } else { + // insert the extent fieldset into the layer control container in + // the calculated position + if (position === 0) { + extentsRootFieldset.insertAdjacentElement( + 'afterbegin', + this._layerControlHTML + ); + } else if (position > 0) { + this.parentNode + .querySelectorAll('map-extent:not([hidden])') + [position - 1]._layerControlHTML.insertAdjacentElement( + 'afterend', + this._layerControlHTML + ); + } + } + this._validateLayerControlContainerHidden(); + } + break; } - break; - } + }) + .catch((reason) => { + console.log( + reason, + `\nin mapExtent.attributeChangeCallback when changing attribute ${name}` + ); + }); } constructor() { // Always call super first in constructor super(); } - connectedCallback() { + async connectedCallback() { + // this.parentNode.host returns the layer- element when parentNode is + // the shadow root + this._createLayerControlExtentHTML = + M._createLayerControlExtentHTML.bind(this); + this.parentLayer = + this.parentNode.nodeName.toUpperCase() === 'LAYER-' + ? this.parentNode + : this.parentNode.host; + if ( + this.hasAttribute('data-moving') || + this.parentLayer.hasAttribute('data-moving') + ) + return; if ( this.querySelector('map-link[rel=query], map-link[rel=features]') && !this.shadowRoot ) { this.attachShadow({ mode: 'open' }); } - let parentLayer = - this.parentNode.nodeName.toUpperCase() === 'LAYER-' - ? this.parentNode - : this.parentNode.host; - parentLayer - .whenReady() - .then(() => { - this._layer = parentLayer._layer; - }) - .catch(() => { - throw new Error('Layer never became ready'); - }); + await this.parentLayer.whenReady(); + // when projection is changed, the parent layer-._layer is created (so whenReady is fulfilled) but then removed, + // then the map-extent disconnectedCallback will be triggered by layer-._onRemove() (clear the shadowRoot) + // even before connectedCallback is finished + // in this case, the microtasks triggered by the fulfillment of the removed MapMLLayer should be stopped as well + // !this.isConnected <=> the disconnectedCallback has run before + if (!this.isConnected) return; + this._layer = this.parentLayer._layer; + this._map = this._layer._map; + // reset the layer extent + delete this.parentLayer.bounds; + this._templateVars = this._initTemplateVars( + // read map-meta[name=extent] from shadowroot or layer- + // querySelector / querySelectorAll on layer- cannot get elements inside its shadowroot + this.parentLayer.shadowRoot + ? this.parentLayer.shadowRoot.querySelector( + 'map-extent > map-meta[name=extent]' + ) || + this.parentLayer.shadowRoot.querySelector('map-meta[name=extent]') + : this.parentLayer.querySelector( + 'map-extent > map-meta[name=extent]' + ) || this.parentLayer.querySelector('map-meta[name=extent]'), + this.units, + this._layer._content, + this._layer.getBase(), + this.units === this._layer.options.mapprojection + ); + this._changeHandler = this._handleChange.bind(this); + this.parentLayer.addEventListener('map-change', this._changeHandler); + // this._opacity is used to record the current opacity value (with or without updates), + // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 + this._opacity = +(this.getAttribute('opacity') || 1.0); + this._templatedLayer = M.templatedLayer(this._templateVars, { + pane: this._layer._container, + opacity: this.opacity, + _leafletLayer: this._layer, + crs: this._layer._properties.crs, + extentZIndex: Array.from( + this.parentLayer.querySelectorAll('map-extent') + ).indexOf(this), + // when a migrates from a remote mapml file and attaches to the shadow of + // this._properties._mapExtents[i] refers to the in remote mapml + extentEl: this._DOMnode || this + }); + // this._layerControlHTML is the fieldset for the extent in the LayerControl + this._layerControlHTML = this._createLayerControlExtentHTML(); + if (!this.hidden) + this._layer.addExtentToLayerControl(this._layerControlHTML); + this._validateLayerControlContainerHidden(); + if (this._templatedLayer._queries) { + if (!this._layer._properties._queries) + this._layer._properties._queries = []; + this._layer._properties._queries = + this._layer._properties._queries.concat(this._templatedLayer._queries); + } + this._calculateBounds(); + } + getLayerControlHTML() { + return this._layerControlHTML; + } + _projectionMatch() { + return ( + this.units.toUpperCase() === + this._layer.options.mapprojection.toUpperCase() + ); + } + _validateDisabled() { + if (!this._templatedLayer) return; + const noTemplateVisible = () => { + let totalTemplateCount = this._templatedLayer._templates.length, + disabledTemplateCount = 0; + for (let j = 0; j < this._templatedLayer._templates.length; j++) { + if (this._templatedLayer._templates[j].rel === 'query') { + continue; + } + if (!this._templatedLayer._templates[j].layer.isVisible) { + disabledTemplateCount++; + } + } + return disabledTemplateCount === totalTemplateCount; + }; + if (!this._projectionMatch() || noTemplateVisible()) { + this.setAttribute('disabled', ''); + this.disabled = true; + } else { + this.removeAttribute('disabled'); + this.disabled = false; + } + this.toggleLayerControlDisabled(); + return this.disabled; + } + + // disable/italicize layer control elements based on the map-extent.disabled property + toggleLayerControlDisabled() { + let input = this._layerControlCheckbox, + label = this._layerControlLabel, // access to the label for the specific map-extent + opacityControl = this._opacityControl, + opacitySlider = this._opacitySlider, + selectDetails = this._selectdetails; + if (this.disabled) { + // update the status of layerControl + input.disabled = true; + opacitySlider.disabled = true; + label.style.fontStyle = 'italic'; + opacityControl.style.fontStyle = 'italic'; + if (selectDetails) { + selectDetails.forEach((i) => { + i.querySelectorAll('select').forEach((j) => { + j.disabled = true; + j.style.fontStyle = 'italic'; + }); + i.style.fontStyle = 'italic'; + }); + } + } else { + input.disabled = false; + opacitySlider.disabled = false; + label.style.fontStyle = 'normal'; + opacityControl.style.fontStyle = 'normal'; + if (selectDetails) { + selectDetails.forEach((i) => { + i.querySelectorAll('select').forEach((j) => { + j.disabled = false; + j.style.fontStyle = 'normal'; + }); + i.style.fontStyle = 'normal'; + }); + } + } } - disconnectedCallback() {} + + _initTemplateVars(metaExtent, projection, mapml, base, projectionMatch) { + 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]) + ); + } + + option.innerHTML = options[i].innerHTML; + select.appendChild(option); + } + return select; + } + var templateVars = []; + // set up the URL template and associated inputs (which yield variable values when processed) + var tlist = this.querySelectorAll( + 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' + ), + varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), + zoomInput = this.querySelector('map-input[type="zoom" i]'), + includesZoom = false, + boundsFallback = {}; + + boundsFallback.zoom = 0; + if (metaExtent) { + let content = M._metaContentToObject(metaExtent.getAttribute('content')), + cs; + + boundsFallback.zoom = content.zoom || boundsFallback.zoom; + + 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); + boundsFallback.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]}`] + ) + ), + boundsFallback.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; + boundsFallback.bounds = fallbackProjection.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 = M.BLANK_TT_TREF; + let blankInputs = mapml.querySelectorAll('map-input'); + for (let i of blankInputs) { + template += `{${i.getAttribute('name')}}`; + } + } + + 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 = this.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', boundsFallback.zoom); + } + let axis = inp.getAttribute('axis'), + axisBounds = M.convertPCRSBounds( + boundsFallback.bounds, + boundsFallback.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', this.redraw, this); + + if (!this._userInputs) { + this._userInputs = []; + } + this._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 mapExtent, + // 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 === M.BLANK_TT_TREF + ) { + if (trel === 'query') { + this._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, + boundsFallbackPCRS: { bounds: boundsFallback.bounds }, + projectionMatch: projectionMatch, + projection: this.units || M.FALLBACK_PROJECTION, + tms: tms, + step: step + }); + } + } + return templateVars; + } + redraw() { + this._templatedLayer.redraw(); + } + + _handleChange() { + // if the parent layer- is checked, add _templatedLayer to map if map-extent is checked, otherwise remove it + if (this.checked && this.parentLayer.checked && !this.disabled) { + this._templatedLayer.addTo(this._layer._map); + this._templatedLayer.setZIndex( + Array.from(this.parentLayer.querySelectorAll('map-extent')).indexOf( + this + ) + ); + } else { + this._map.removeLayer(this._templatedLayer); + } + // change the checkbox in the layer control to match map-extent.checked + // doesn't trigger the event handler because it's not user-caused AFAICT + } + _validateLayerControlContainerHidden() { + let extentsFieldset = this.parentLayer._propertiesGroupAnatomy; + let nodeToSearch = this.parentLayer.shadowRoot || this.parentLayer; + if ( + nodeToSearch.querySelectorAll('map-extent:not([hidden])').length === 0 + ) { + extentsFieldset.setAttribute('hidden', ''); + } else { + extentsFieldset.removeAttribute('hidden'); + } + } + disconnectedCallback() { + // in case of projection change, the disconnectedcallback will be triggered by removing layer-._layer even before + // map-extent.connectedcallback is finished (because it will wait for the layer- to be ready) + // !this._templatedLayer <=> this.connectedCallback has not yet been finished before disconnectedCallback is triggered + if ( + this.hasAttribute('data-moving') || + this.parentLayer.hasAttribute('data-moving') || + !this._templatedLayer + ) + return; + this._validateLayerControlContainerHidden(); + // remove layer control for map-extent from layer control DOM + this._layerControlHTML.remove(); + this._map.removeLayer(this._templatedLayer); + this.parentLayer.removeEventListener('map-change', this._changeHandler); + delete this._templatedLayer; + delete this.parentLayer.bounds; + } + _calculateBounds() { + let bounds = null, + zoomMax = 0, + zoomMin = 0, + maxNativeZoom = 0, + minNativeZoom = 0; + // bounds should be able to be calculated unconditionally, not depend on map-extent.checked + for (let j = 0; j < this._templateVars.length; j++) { + let inputData = M._extractInputBounds(this._templateVars[j]); + this._templateVars[j].tempExtentBounds = inputData.bounds; + this._templateVars[j].extentZoomBounds = inputData.zoomBounds; + if (!bounds) { + bounds = this._templateVars[j].tempExtentBounds; + zoomMax = this._templateVars[j].extentZoomBounds.maxZoom; + zoomMin = this._templateVars[j].extentZoomBounds.minZoom; + maxNativeZoom = this._templateVars[j].extentZoomBounds.maxNativeZoom; + minNativeZoom = this._templateVars[j].extentZoomBounds.minNativeZoom; + } else { + bounds.extend(this._templateVars[j].tempExtentBounds.min); + bounds.extend(this._templateVars[j].tempExtentBounds.max); + zoomMax = Math.max( + zoomMax, + this._templateVars[j].extentZoomBounds.maxZoom + ); + zoomMin = Math.min( + zoomMin, + this._templateVars[j].extentZoomBounds.minZoom + ); + maxNativeZoom = Math.max( + maxNativeZoom, + this._templateVars[j].extentZoomBounds.maxNativeZoom + ); + minNativeZoom = Math.min( + minNativeZoom, + this._templateVars[j].extentZoomBounds.minNativeZoom + ); + } + } + // cannot be named as layerBounds if we decide to keep the debugoverlay logic + this._templatedLayer.bounds = bounds; + this._templatedLayer.zoomBounds = { + minZoom: zoomMin, + maxZoom: zoomMax, + maxNativeZoom, + minNativeZoom + }; + } + whenReady() { return new Promise((resolve, reject) => { let interval, failureTimer; - if (this._layer) { + if (this._templatedLayer) { resolve(); } else { let extentElement = this; @@ -99,10 +590,14 @@ export class MapExtent extends HTMLElement { failureTimer = setTimeout(extentNotDefined, 10000); } function testForExtent(extentElement) { - if (extentElement._layer) { + if (extentElement._templatedLayer) { clearInterval(interval); clearTimeout(failureTimer); resolve(); + } else if (!extentElement.isConnected) { + clearInterval(interval); + clearTimeout(failureTimer); + reject('map-extent was disconnected while waiting to be ready'); } } function extentNotDefined() { diff --git a/src/map-feature.js b/src/map-feature.js index 9f00a74f7..097040132 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -100,38 +100,55 @@ export class MapFeature extends HTMLElement { } connectedCallback() { - // if mapFeature element is not connected to layer- or layer-'s shadowroot, - // or the parent layer- element has a "data-moving" attribute - if ( - (this.parentNode.nodeType !== document.DOCUMENT_FRAGMENT_NODE && - this.parentNode.nodeName.toLowerCase() !== 'layer-') || - (this.parentNode.nodeType === document.DOCUMENT_FRAGMENT_NODE && - this.parentNode.host.hasAttribute('data-moving')) || - (this.parentNode.nodeName.toLowerCase() === 'layer-' && - this.parentNode.hasAttribute('data-moving')) - ) { - return; - } - // set up the map-feature object properties - this._addFeature(); - // use observer to monitor the changes in mapFeature's subtree - // (i.e. map-properties, map-featurecaption, map-coordinates) - this._observer = new MutationObserver((mutationList) => { - for (let mutation of mutationList) { - // the attributes changes of element should be handled by attributeChangedCallback() - if (mutation.type === 'attributes' && mutation.target === this) { - return; - } - // re-render feature if there is any observed change - this._reRender(); + // if the features are connected to the remote mapml + if (this.closest('mapml-')) return; + this._parentEl = + this.parentNode.nodeName.toUpperCase() === 'LAYER-' || + this.parentNode.nodeName.toUpperCase() === 'MAP-EXTENT' + ? this.parentNode + : this.parentNode.host; + this._parentEl.whenReady().then(() => { + this._layer = this._parentEl._layer; + delete this._parentEl.bounds; + if ( + this._layer._layerEl.hasAttribute('data-moving') || + this._parentEl.hasAttribute('data-moving') + ) + return; + // if mapFeature element is not connected to layer- or layer-'s shadowroot, + // or the parent layer- element has a "data-moving" attribute + if ( + (this.parentNode.nodeType !== document.DOCUMENT_FRAGMENT_NODE && + this.parentNode.nodeName.toLowerCase() !== 'layer-') || + (this.parentNode.nodeType === document.DOCUMENT_FRAGMENT_NODE && + this.parentNode.host.hasAttribute('data-moving')) || + (this.parentNode.nodeName.toLowerCase() === 'layer-' && + this.parentNode.hasAttribute('data-moving')) + ) { + return; } - }); - this._observer.observe(this, { - childList: true, - subtree: true, - attributes: true, - attributeOldValue: true, - characterData: true + // set up the map-feature object properties + this._addFeature(); + // use observer to monitor the changes in mapFeature's subtree + // (i.e. map-properties, map-featurecaption, map-coordinates) + this._observer = new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + // the attributes changes of element should be handled by attributeChangedCallback() + if (mutation.type === 'attributes' && mutation.target === this) { + return; + } + // re-render feature if there is any observed change + this._reRender(); + delete this._parentEl.bounds; + } + }); + this._observer.observe(this, { + childList: true, + subtree: true, + attributes: true, + attributeOldValue: true, + characterData: true + }); }); } @@ -140,6 +157,7 @@ export class MapFeature extends HTMLElement { if (this._layer._layerEl.hasAttribute('data-moving')) return; this._removeFeature(); this._observer.disconnect(); + delete this._parentEl.bounds; } _reRender() { @@ -155,7 +173,6 @@ export class MapFeature extends HTMLElement { .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(); } @@ -178,7 +195,6 @@ export class MapFeature extends HTMLElement { if (mapmlvectors._features[this.zoom]) { this._removeInFeatureList(this.zoom); } - let container = this._layer.shadowRoot || this._layer._layerEl; // update zoom bounds of vector layer mapmlvectors.zoomBounds = M.getZoomBounds( this._layer._content, @@ -196,18 +212,11 @@ export class MapFeature extends HTMLElement { } _addFeature() { - this._parentEl = - this.parentNode.nodeName.toUpperCase() === 'LAYER-' || - this.parentNode.nodeName.toUpperCase() === 'MAP-EXTENT' - ? this.parentNode - : this.parentNode.host; - this._parentEl.whenReady().then(() => { let parentLayer = this._parentEl.nodeName.toUpperCase() === 'LAYER-' ? this._parentEl : this._parentEl.parentElement || this._parentEl.parentNode.host; - this._layer = parentLayer._layer; this._map = this._layer._map; let mapmlvectors = this._layer._mapmlvectors; // "synchronize" the event handlers between map-feature and @@ -239,12 +248,6 @@ export class MapFeature extends HTMLElement { } } - // Number of features that are being displayed on the map - let renderedFeatureCount = Object.keys(mapmlvectors._layers).length; - // 0 because feature could be hidden by the min/max attr., 1 so as other features are added, _setLayerElExtent() is not run multiple times - if (renderedFeatureCount === 1 || renderedFeatureCount === 0) { - this._layer._setLayerElExtent(); - } this._setUpEvents(); }); } diff --git a/src/map-input.js b/src/map-input.js index bd92e01b1..bb3472829 100644 --- a/src/map-input.js +++ b/src/map-input.js @@ -1,5 +1,4 @@ -import { MapLink } from './map-link.js'; - +/* global M */ export class MapInput extends HTMLElement { static get observedAttributes() { return [ @@ -33,7 +32,7 @@ export class MapInput extends HTMLElement { } } get value() { - return this.getAttribute('value'); + return this.input.getValue(); } set value(val) { if (val) { @@ -73,7 +72,26 @@ export class MapInput extends HTMLElement { } } get min() { - return this.getAttribute('min'); + if ( + this.type === 'height' || + this.type === 'width' || + this.type === 'hidden' + ) { + return null; + } + if (this.getAttribute('min')) { + return this.getAttribute('min'); + } else if (this._layer._layerEl.querySelector('map-meta[name=zoom]')) { + // fallback map-meta on layer + return M._metaContentToObject( + this._layer._layerEl + .querySelector('map-meta[name=zoom]') + .getAttribute('content') + ).min; + } else { + // fallback map min + return this._layer._layerEl.extent.zoom.minZoom.toString(); + } } set min(val) { if (val) { @@ -81,7 +99,26 @@ export class MapInput extends HTMLElement { } } get max() { - return this.getAttribute('max'); + if ( + this.type === 'height' || + this.type === 'width' || + this.type === 'hidden' + ) { + return null; + } + if (this.getAttribute('max')) { + return this.getAttribute('max'); + } else if (this._layer._layerEl.querySelector('map-meta[name=zoom]')) { + // fallback map-meta on layer + return M._metaContentToObject( + this._layer._layerEl + .querySelector('map-meta[name=zoom]') + .getAttribute('content') + ).max; + } else { + // fallback map max + return this._layer._layerEl.extent.zoom.maxZoom.toString(); + } } set max(val) { if (val) { @@ -89,7 +126,11 @@ export class MapInput extends HTMLElement { } } get step() { - return this.getAttribute('step'); + if (this.type !== 'zoom') { + return null; + } else { + return this.getAttribute('step') || '1'; + } } set step(val) { if (val) { @@ -97,64 +138,197 @@ export class MapInput extends HTMLElement { } } attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case 'name': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'type': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'value': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'axis': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'units': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'position': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'rel': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'min': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'max': - if (oldValue !== newValue) { - // handle side effects + this.whenReady() + .then(() => { + switch (name) { + case 'name': + if (oldValue !== newValue) { + // update associated class value on attribute change + if (oldValue !== null) { + this.input.name = newValue; + } + } + break; + case 'type': + if (oldValue !== newValue) { + // handle side effects + // not allowed to change 'type' + } + break; + case 'value': + if (oldValue !== newValue) { + if (oldValue !== null) { + this.input.value = newValue; + } else { + this.initialValue = newValue; + } + } + break; + case 'axis': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.axis = newValue; + } + break; + case 'units': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.units = newValue; + } + break; + case 'position': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.position = newValue; + } + break; + case 'rel': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.rel = newValue; + } + break; + case 'min': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.min = newValue; + } + break; + case 'max': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.max = newValue; + } + break; + case 'step': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.step = newValue; + } + break; } - break; - case 'step': - if (oldValue !== newValue) { - // handle side effects - } - break; - } + }) + .catch((reason) => { + console.log( + reason, + `\nin mapInput.attributeChangeCallback when changing attribute ${name}` + ); + }); } constructor() { // Always call super first in constructor super(); } - connectedCallback() {} + connectedCallback() { + this.parentElement + .whenReady() + .then(() => { + if (this.parentElement.nodeName === 'MAP-EXTENT') { + this._layer = this.parentElement._layer; + } + switch (this.type) { + case 'zoom': + // input will store the input Class specific to the input type + this.input = new M.ZoomInput( + this.name, + this.min, + this.max, + this.initialValue, + this.step, + this._layer + ); + break; + case 'location': + // input will store the input Class specific to the input type + this.input = new M.LocationInput( + this.name, + this.position, + this.axis, + this.units, + this.min, + this.max, + this.rel, + this._layer + ); + break; + case 'width': + // input will store the input Class specific to the input type + this.input = new M.WidthInput(this.name, this._layer); + break; + case 'height': + // input will store the input Class specific to the input type + this.input = new M.HeightInput(this.name, this._layer); + break; + case 'hidden': + // input will store the input Class specific to the input type + this.input = new M.HiddenInput(this.name, this.initialValue); + break; + } + }) + .catch((reason) => { + console.log(reason, '\nin mapInput.connectedCallback'); + }); + } disconnectedCallback() {} + + //https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity + checkValidity() { + if (this.input.validateInput()) { + return true; + } else { + const evt = new Event('invalid', { + bubbles: true, + cancelable: true, + composed: true + }); + this.dispatchEvent(evt); + return false; + } + } + + //https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity + reportValidity() { + if (this.input.validateInput()) { + return true; + } else { + const evt = new Event('invalid', { + bubbles: true, + cancelable: true, + composed: true + }); + this.dispatchEvent(evt); + //if the event isn't canceled reports the problem to the user. + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-reportvalidity-dev + console.log("Input type='" + this.type + "' is not valid!"); + return false; + } + } + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this.input) { + resolve(); + } else { + let inputElement = this; + interval = setInterval(testForInput, 300, inputElement); + failureTimer = setTimeout(inputNotDefined, 10000); + } + function testForInput(inputElement) { + if (inputElement.input) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } else if (!inputElement.isConnected) { + clearInterval(interval); + clearTimeout(failureTimer); + reject('map-input was disconnected while waiting to be ready'); + } + } + function inputNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for input to be ready'); + } + }); + } } -window.customElements.define('map-input', MapInput); diff --git a/src/map-link.js b/src/map-link.js index 969bf7a41..2ebf0ed70 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -149,5 +149,30 @@ export class MapLink extends HTMLElement { } connectedCallback() {} disconnectedCallback() {} + + // Resolve the templated URL with info from the sibling map-input's + resolve() { + if (this.tref) { + let obj = {}; + const inputs = this.parentElement.querySelectorAll('map-input'); + if (this.rel === 'image') { + // image/map + for (let i = 0; i < inputs.length; i++) { + const inp = inputs[i]; + obj[inp.name] = inp.value; + } + console.log(obj); // DEBUGGING + return L.Util.template(this.tref, obj); + } else if (this.rel === 'tile') { + // TODO. Need to get tile coords from moveend + // should be done/called from the TemplatedTilelayer.js file + return obj; + } else if (this.rel === 'query') { + // TODO. Need to get the click coords from click event + // should be done/called from the templatedlayer.js file + } else if (this.rel === 'features') { + // TODO. + } + } + } } -window.customElements.define('map-link', MapLink); diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 4fe1e7ebe..cdb0e0100 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -1,10 +1,11 @@ import './leaflet.js'; // bundled with proj4, proj4leaflet, modularized import './mapml.js'; -import DOMTokenList from './DOMTokenList.js'; import { MapLayer } from './layer.js'; import { MapCaption } from './map-caption.js'; import { MapFeature } from './map-feature.js'; import { MapExtent } from './map-extent.js'; +import { MapInput } from './map-input.js'; +import { MapLink } from './map-link.js'; export class MapViewer extends HTMLElement { static get observedAttributes() { @@ -44,21 +45,21 @@ export class MapViewer extends HTMLElement { this.setAttribute('controlslist', value); } get width() { - return window.getComputedStyle(this).width.replace('px', ''); + return +window.getComputedStyle(this).width.replace('px', ''); } set width(val) { //img.height or img.width setters change or add the corresponding attributes this.setAttribute('width', val); } get height() { - return window.getComputedStyle(this).height.replace('px', ''); + return +window.getComputedStyle(this).height.replace('px', ''); } set height(val) { //img.height or img.width setters change or add the corresponding attributes this.setAttribute('height', val); } get lat() { - return this.hasAttribute('lat') ? this.getAttribute('lat') : '0'; + return +(this.hasAttribute('lat') ? this.getAttribute('lat') : 0); } set lat(val) { if (val) { @@ -66,7 +67,7 @@ export class MapViewer extends HTMLElement { } } get lon() { - return this.hasAttribute('lon') ? this.getAttribute('lon') : '0'; + return +(this.hasAttribute('lon') ? this.getAttribute('lon') : 0); } set lon(val) { if (val) { @@ -90,7 +91,7 @@ export class MapViewer extends HTMLElement { } } get zoom() { - return this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0; + return +(this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0); } set zoom(val) { var parsedVal = parseInt(val, 10); @@ -145,7 +146,7 @@ export class MapViewer extends HTMLElement { .then(() => { this._initShadowRoot(); - this._controlsList = new DOMTokenList( + this._controlsList = new M.DOMTokenList( this.getAttribute('controlslist'), this, 'controlslist', @@ -357,6 +358,7 @@ export class MapViewer extends HTMLElement { // 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. + // leaflet doesn't like this: https://github.com/Leaflet/Leaflet/issues/2553 this._map.options.crs = M[newValue]; this._map.options.projection = newValue; let layersReady = []; @@ -371,9 +373,13 @@ export class MapViewer extends HTMLElement { // 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/ + // see leaflet bug: https://github.com/Leaflet/Leaflet/issues/2553 this.zoomTo(lat, lon, zoom); - this._resetHistory(); - this._map.announceMovement.enable(); + if (M.options.announceMovement) + this._map.announceMovement.enable(); + this.querySelectorAll('layer-').forEach((layer) => { + layer.dispatchEvent(new CustomEvent('map-change')); + }); }); } }; @@ -383,6 +389,14 @@ export class MapViewer extends HTMLElement { connect(); resolve(); }).then(() => { + if (this._map && this._map.options.projection !== oldValue) { + // this awful hack is brought to you by a leaflet bug/ feature request + // https://github.com/Leaflet/Leaflet/issues/2553 + this.zoomTo(this.lat, this.lon, this.zoom + 1); + this.zoomTo(this.lat, this.lon, this.zoom - 1); + // this doesn't completely work either + this._resetHistory(); + } if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); }); } @@ -1148,7 +1162,7 @@ export class MapViewer extends HTMLElement { if (M[t.projection.toUpperCase()]) return t.projection.toUpperCase(); let tileSize = [256, 512, 1024, 2048, 4096].includes(t.tilesize) ? t.tilesize - : 256; + : M.TILE_SIZE; M[t.projection] = new L.Proj.CRS(t.projection, t.proj4string, { origin: t.origin, @@ -1344,10 +1358,11 @@ export class MapViewer extends HTMLElement { } }); } - async whenLayersReady() { + whenLayersReady() { let layersReady = []; + // check if all the children elements (map-extent, map-feature) of all layer- are ready for (let layer of [...this.layers]) { - layersReady.push(layer.whenReady()); + layersReady.push(layer.whenElemsReady()); } return Promise.allSettled(layersReady); } @@ -1389,3 +1404,5 @@ window.customElements.define('layer-', MapLayer); window.customElements.define('map-caption', MapCaption); window.customElements.define('map-feature', MapFeature); window.customElements.define('map-extent', MapExtent); +window.customElements.define('map-input', MapInput); +window.customElements.define('map-link', MapLink); diff --git a/src/mapml.css b/src/mapml.css index eaa5990e3..3509685f1 100644 --- a/src/mapml.css +++ b/src/mapml.css @@ -52,6 +52,14 @@ } } +/* for extents that don't overlap the viewport, the extent and it's layer +control fieldset become disabled. The descendant fieldsets also become +disabled (but they / their descendant content are not explicitly italicized by +the browser)' */ +.leaflet-control-layers-overlays fieldset:disabled span.mapml-layer-item-name { + font-style: italic; +} + /* Generic class for seamless buttons */ .mapml-button { background-color: transparent; @@ -855,7 +863,7 @@ label.mapml-layer-item-toggle { padding-inline-start: 0; } -.mapml-layer-item-settings .mapml-layer-extent .mapml-layer-item-opacity { +.mapml-layer-item-settings .mapml-layer-extent .mapml-layer-item-details { padding-inline-start: 1.6rem; } diff --git a/src/mapml/control/LayerControl.js b/src/mapml/control/LayerControl.js index 2f5404070..b524fd353 100644 --- a/src/mapml/control/LayerControl.js +++ b/src/mapml/control/LayerControl.js @@ -27,8 +27,6 @@ export var LayerControl = L.Control.Layers.extend({ }, onAdd: function () { this._initLayout(); - this._map.on('validate', this._validateInput, this); - L.DomEvent.on(this.options.mapEl, 'layerchange', this._validateInput, this); // Adding event on layer control button L.DomEvent.on( this._container.getElementsByTagName('a')[0], @@ -43,7 +41,6 @@ export var LayerControl = L.Control.Layers.extend({ this ); this._update(); - //this._validateExtents(); if (this._layers.length < 1 && !this._map._showControls) { this._container.setAttribute('hidden', ''); } else { @@ -52,18 +49,12 @@ export var LayerControl = L.Control.Layers.extend({ return this._container; }, onRemove: function (map) { - map.off('validate', this._validateInput, this); L.DomEvent.off( this._container.getElementsByTagName('a')[0], 'keydown', this._focusFirstLayer, this._container ); - // remove layer-registerd event handlers so that if the control is not - // 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); - } }, addOrUpdateOverlay: function (layer, name) { var alreadyThere = false; @@ -90,49 +81,8 @@ export var LayerControl = L.Control.Layers.extend({ this._container.setAttribute('hidden', ''); } }, - _validateInput: function (e) { - for (let i = 0; i < this._layers.length; i++) { - if (!this._layers[i].input.labels[0]) continue; - let label = this._layers[i].input.labels[0].getElementsByTagName('span'), - input = this._layers[i].input.labels[0].getElementsByTagName('input'); - input[0].checked = this._layers[i].layer._layerEl.checked; - if ( - this._layers[i].layer._layerEl.disabled && - this._layers[i].layer._layerEl.checked - ) { - input[0].closest('fieldset').disabled = true; - label[0].style.fontStyle = 'italic'; - } else { - input[0].closest('fieldset').disabled = false; - label[0].style.fontStyle = 'normal'; - } - // check if an extent is disabled and disable it - if ( - this._layers[i].layer._properties && - this._layers[i].layer._properties._mapExtents - ) { - for ( - let j = 0; - j < this._layers[i].layer._properties._mapExtents.length; - j++ - ) { - let input = - this._layers[i].layer._properties._mapExtents[j].extentAnatomy, - label = input.getElementsByClassName('mapml-layer-item-name')[0]; - if ( - this._layers[i].layer._properties._mapExtents[j].disabled && - this._layers[i].layer._properties._mapExtents[j].checked - ) { - label.style.fontStyle = 'italic'; - input.disabled = true; - } else { - label.style.fontStyle = 'normal'; - input.disabled = false; - } - } - } - } - }, + + _checkDisabledLayers: function () {}, // focus the first layer in the layer control when enter is pressed _focusFirstLayer: function (e) { @@ -152,7 +102,7 @@ export var LayerControl = L.Control.Layers.extend({ return range.min <= zoom && zoom <= range.max; }, _addItem: function (obj) { - var layercontrols = obj.layer.getLayerUserControlsHTML(); + var layercontrols = obj.layer._layerEl._layerControlHTML; // the input is required by Leaflet... obj.input = layercontrols.querySelector( 'input.leaflet-control-layers-selector' @@ -161,7 +111,6 @@ export var LayerControl = L.Control.Layers.extend({ this._layerControlInputs.push(obj.input); obj.input.layerId = L.stamp(obj.layer); - L.DomEvent.on(obj.input, 'click', this._onInputClick, this); this._overlaysList.appendChild(layercontrols); return layercontrols; }, diff --git a/src/mapml/elementSupport/extents/createLayerControlForExtent.js b/src/mapml/elementSupport/extents/createLayerControlForExtent.js new file mode 100644 index 000000000..80c76df89 --- /dev/null +++ b/src/mapml/elementSupport/extents/createLayerControlForExtent.js @@ -0,0 +1,282 @@ +export var createLayerControlExtentHTML = function () { + 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-details mapml-control-layers', + extentSettings + ), + extentOpacitySummary = L.DomUtil.create('summary', '', opacityControl), + mapEl = this.parentLayer.parentNode, + layerEl = this.parentLayer, + opacity = L.DomUtil.create('input', '', opacityControl); + extentSettings.hidden = true; + extent.setAttribute('aria-grabbed', 'false'); + + // 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); + + if (this._userInputs) { + var frag = document.createDocumentFragment(); + var templates = this._templateVars; + if (templates) { + this._selectdetails = []; + 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-details 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); + this._selectdetails.push(selectdetails); + } + } + } + } + extentSettings.appendChild(frag); + } + + 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'); + removeExtentButton.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.remove(); + }); + + 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-extent-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-extent-item-opacity-' + L.stamp(extentOpacitySummary) + ); + const changeOpacity = function (e) { + if (e && e.target && e.target.value >= 0 && e.target.value <= 1.0) { + this._templatedLayer.changeOpacity(e.target.value); + } + }; + opacity.setAttribute('value', this.opacity); + opacity.value = this._templatedLayer._container.style.opacity || '1.0'; + opacity.addEventListener('change', changeOpacity.bind(this)); + + var extentItemNameSpan = L.DomUtil.create( + 'span', + 'mapml-layer-item-name', + extentLabel + ); + input.type = 'checkbox'; + input.defaultChecked = this.checked; + extentItemNameSpan.innerHTML = this.label; + const changeCheck = function () { + this.checked = !this.checked; + }; + // save for later access by API + this._layerControlCheckbox = input; + input.addEventListener('change', changeCheck.bind(this)); + extentItemNameSpan.id = + 'mapml-extent-item-name-{' + L.stamp(extentItemNameSpan) + '}'; + extent.setAttribute('aria-labelledby', extentItemNameSpan.id); + extentItemNameSpan.extent = this; + + 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, + originalPosition = Array.from( + extent.parentElement.querySelectorAll('fieldset') + ).indexOf(extent); + + 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) > 15 || 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'); + + 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); + } + }; + + document.body.ontouchend = document.body.onmouseup = () => { + let newPosition = Array.from( + extent.parentElement.querySelectorAll('fieldset') + ).indexOf(extent); + control.setAttribute('aria-grabbed', 'false'); + control.removeAttribute('aria-dropeffect'); + control.style.pointerEvents = null; + control.style.transform = null; + if (originalPosition !== newPosition) { + 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; + }; + } + }; + this._extentRootFieldset = extent; + this._opacitySlider = opacity; + this._opacityControl = opacityControl; + this._layerControlLabel = extentLabel; + return extent; +}; diff --git a/src/mapml/elementSupport/inputs/heightInput.js b/src/mapml/elementSupport/inputs/heightInput.js new file mode 100644 index 000000000..3629b0ddd --- /dev/null +++ b/src/mapml/elementSupport/inputs/heightInput.js @@ -0,0 +1,18 @@ +export class HeightInput { + constructor(name, layer) { + this.name = name; + this.layer = layer; + } + + validateInput() { + // name is required + if (!this.name) { + return false; + } + return true; + } + + getValue() { + return this.layer._map.getSize().y; + } +} diff --git a/src/mapml/elementSupport/inputs/hiddenInput.js b/src/mapml/elementSupport/inputs/hiddenInput.js new file mode 100644 index 000000000..0919ef90f --- /dev/null +++ b/src/mapml/elementSupport/inputs/hiddenInput.js @@ -0,0 +1,19 @@ +export class HiddenInput { + constructor(name, value) { + this.name = name; + this.value = value; + } + + validateInput() { + // name is required + // value is required + if (!this.name || !this.value) { + return false; + } + return true; + } + + getValue() { + return this.value; + } +} diff --git a/src/mapml/elementSupport/inputs/locationInput.js b/src/mapml/elementSupport/inputs/locationInput.js new file mode 100644 index 000000000..fb4e7b880 --- /dev/null +++ b/src/mapml/elementSupport/inputs/locationInput.js @@ -0,0 +1,116 @@ +export class LocationInput { + constructor(name, position, axis, units, min, max, rel, layer) { + this.name = name; + this.position = position; + this.axis = axis; + // if unit/cs not present, find it + if (!units && axis && !['i', 'j'].includes(axis)) { + this.units = M.axisToCS(axis).toLowerCase(); + } else { + this.units = units; // cs + } + this.min = min; + this.max = max; + this.rel = rel; + this.layer = layer; + } + + validateInput() { + // name is required + // axis is required + if (!this.name || !this.axis) { + return false; + } + // cs/units is only required when the axis is i/j. To differentiate between the units/cs + if ( + (this.axis === 'i' || this.axis === 'j') && + !['map', 'tile'].includes(this.units) + ) { + return false; + } + // check if axis match the units/cs + if (this.units) { + let axisCS = M.axisToCS(this.axis); + if ( + typeof axisCS === 'string' && + axisCS.toUpperCase() !== this.units.toUpperCase() + ) { + return false; + } + } + // position is not required, will default to top-left + // min max fallbacks, map-meta -> projection + // rel not required, default is image/extent + return true; + } + + _TCRSToPCRS(coords, zoom) { + // TCRS pixel point to Projected CRS point (in meters, presumably) + var map = this.layer._map, + crs = map.options.crs, + loc = crs.transformation.untransform(coords, crs.scale(zoom)); + return loc; + } + + getValue(zoom = undefined, bounds = undefined) { + // units = cs + // + if (zoom === undefined) zoom = this.layer._map.getZoom(); + if (bounds === undefined) bounds = this.layer._map.getPixelBounds(); + + if (this.units === 'pcrs' || this.units === 'gcrs') { + switch (this.axis) { + case 'longitude': + case 'easting': + if (this.position) { + if (this.position.match(/.*?-left/i)) { + return this._TCRSToPCRS(bounds.min, zoom).x; + } else if (this.position.match(/.*?-right/i)) { + return this._TCRSToPCRS(bounds.max, zoom).x; + } + } else { + // position is not required, will default to top-left + return this._TCRSToPCRS(bounds.min, zoom).x; + } + break; + case 'latitude': + case 'northing': + if (this.position) { + if (this.position.match(/top-.*?/i)) { + return this._TCRSToPCRS(bounds.min, zoom).y; + } else if (this.position.match(/bottom-.*?/i)) { + return this._TCRSToPCRS(bounds.max, zoom).y; + } + } else { + // position is not required, will default to top-left + return this._TCRSToPCRS(bounds.min, zoom).y; + } + break; + } + } else if (this.units === 'tilematrix') { + // Value is retrieved from the createTile method of TemplatedTileLayer, on move end. + // Different values for each tile when filling in the map tiles on the map. + // Currently storing all x,y,z within one object, + // TODO: change return value as needed based on usage by map-input + // https://github.com/Leaflet/Leaflet/blob/6994baf25f267db1c8b720c28a61e0700d0aa0e8/src/layer/tile/GridLayer.js#L652 + const center = this.layer._map.getCenter(); + const templatedTileLayer = this.layer._templatedLayer._templates[0].layer; + const pixelBounds = templatedTileLayer._getTiledPixelBounds(center); + const tileRange = templatedTileLayer._pxBoundsToTileRange(pixelBounds); + let obj = []; + for (let j = tileRange.min.y; j <= tileRange.max.y; j++) { + for (let i = tileRange.min.x; i <= tileRange.max.x; i++) { + const coords = new L.Point(i, j); + coords.z = templatedTileLayer._tileZoom; + obj.push(coords); + } + } + return obj; + } else if (this.units === 'tile' || this.units === 'map') { + // used for query handler on map enter or click. + // mapi, tilei, mapj, tilej used for query handling, value is derived from the mouse click event + // or center of the map when used with keyboard. + } + return; + } +} diff --git a/src/mapml/elementSupport/inputs/widthInput.js b/src/mapml/elementSupport/inputs/widthInput.js new file mode 100644 index 000000000..756f72ffe --- /dev/null +++ b/src/mapml/elementSupport/inputs/widthInput.js @@ -0,0 +1,18 @@ +export class WidthInput { + constructor(name, layer) { + this.name = name; + this.layer = layer; + } + + validateInput() { + // name is required + if (!this.name) { + return false; + } + return true; + } + + getValue() { + return this.layer._map.getSize().x; + } +} diff --git a/src/mapml/elementSupport/inputs/zoomInput.js b/src/mapml/elementSupport/inputs/zoomInput.js new file mode 100644 index 000000000..5ac511675 --- /dev/null +++ b/src/mapml/elementSupport/inputs/zoomInput.js @@ -0,0 +1,26 @@ +export class ZoomInput { + constructor(name, min, max, value, step, layer) { + this.name = name; + this.min = min; + this.max = max; + this.value = value; + this.step = step; + this.layer = layer; + } + + validateInput() { + // name is required + if (!this.name) { + return false; + } + // min and max can not be present + // fallback would be layer's meta, -> projection min, max + // don't need value, map-meta max value, -> fallback is max zoom of projection + // don't need step, defaults to 1 + return true; + } + + getValue() { + return this.layer._map.options.mapEl.zoom; + } +} diff --git a/src/mapml/elementSupport/layers/createLayerControlForLayer.js b/src/mapml/elementSupport/layers/createLayerControlForLayer.js new file mode 100644 index 000000000..e25bb6f5c --- /dev/null +++ b/src/mapml/elementSupport/layers/createLayerControlForLayer.js @@ -0,0 +1,305 @@ +export var createLayerControlHTML = 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.parentNode; + + // 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); + + 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.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 ( + e.target.closest('fieldset').nextElementSibling && + !e.target.closest('fieldset').nextElementSibling.disbaled + ) { + 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._layer + ); + + 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._layer._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._layer + ); + + input.defaultChecked = this.checked; + input.type = 'checkbox'; + input.setAttribute('class', 'leaflet-control-layers-selector'); + layerItemName.layer = this._layer; + const changeCheck = function () { + this.checked = !this.checked; + }; + input.addEventListener('change', changeCheck.bind(this)); + if (this._layer._legendUrl) { + var legendLink = document.createElement('a'); + legendLink.text = ' ' + this._layer._title; + legendLink.href = this._layer._legendUrl; + legendLink.target = '_blank'; + legendLink.draggable = false; + layerItemName.appendChild(legendLink); + } else { + layerItemName.innerHTML = this._layer._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._layer._container.style.opacity || '1.0'); + opacity.setAttribute('step', '0.1'); + opacity.setAttribute( + 'aria-labelledby', + 'mapml-layer-item-opacity-' + L.stamp(opacityControlSummary) + ); + + const changeOpacity = function (e) { + if (e && e.target && e.target.value >= 0 && e.target.value <= 1.0) { + this._layer.changeOpacity(e.target.value); + } + }; + opacity.value = this._layer._container.style.opacity || '1.0'; + opacity.addEventListener('change', changeOpacity.bind(this)); + + 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, + originalPosition = Array.from( + controls.querySelectorAll('fieldset') + ).indexOf(fieldset); + + 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) > 15 || 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'); + + 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); + } + }; + + document.body.ontouchend = document.body.onmouseup = () => { + let newPosition = Array.from( + controls.querySelectorAll('fieldset') + ).indexOf(fieldset); + control.setAttribute('aria-grabbed', 'false'); + control.removeAttribute('aria-dropeffect'); + control.style.pointerEvents = null; + control.style.transform = null; + if (originalPosition !== newPosition) { + 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; + }; + } + }; + + itemToggleLabel.appendChild(input); + itemToggleLabel.appendChild(layerItemName); + itemSettingControlButton.appendChild(settingsButtonNameIcon); + settingsButtonNameIcon.appendChild(svgSettingsControlIcon); + + if (this._layer._styles) { + layerItemSettings.appendChild(this._layer._styles); + } + + this._layerControlCheckbox = input; + this._layerControlLabel = itemToggleLabel; + this._opacityControl = opacityControl; + this._opacitySlider = opacity; + this._layerControlHTML = fieldset; + this._layerItemSettingsHTML = layerItemSettings; + this._propertiesGroupAnatomy = extentsFieldset; + this._styles = this._layer._styles; + extentsFieldset.setAttribute('aria-label', 'Sublayers'); + extentsFieldset.setAttribute('hidden', ''); + layerItemSettings.appendChild(extentsFieldset); + return this._layerControlHTML; +}; diff --git a/src/mapml/handlers/AnnounceMovement.js b/src/mapml/handlers/AnnounceMovement.js index 6edda8e82..921a216c3 100644 --- a/src/mapml/handlers/AnnounceMovement.js +++ b/src/mapml/handlers/AnnounceMovement.js @@ -121,22 +121,27 @@ export var AnnounceMovement = L.Handler.extend({ this._map.dragging._draggable.wasDragged = false; }, - totalBounds: function () { - let layers = Object.keys(this._layers); - let bounds = L.bounds(); - - layers.forEach((i) => { - if (this._layers[i].layerBounds) { - if (!bounds) { - let point = this._layers[i].layerBounds.getCenter(); - bounds = L.bounds(point, point); + totalBounds: function (e) { + // don't bother with non-MapMLLayer layers... + if (!e.layer._layerEl) return; + let map = this.options.mapEl; + map.whenLayersReady().then(() => { + let layers = map.querySelectorAll('layer-'); + let bounds; + for (let i = 0; i < layers.length; i++) { + // the _layer may no longer exist if this is invoked by layerremove + if (layers[i]._layer) { + let extent = layers[i].extent; + if (bounds && extent) { + bounds.extend(M.extentToBounds(extent, 'pcrs')); + } else if (extent) { + bounds = M.extentToBounds(extent, 'pcrs'); + } } - bounds.extend(this._layers[i].layerBounds.min); - bounds.extend(this._layers[i].layerBounds.max); } - }); - this.totalLayerBounds = bounds; + this.totalLayerBounds = bounds; + }); }, dragged: function () { diff --git a/src/mapml/index.js b/src/mapml/index.js index cd05e6b5a..dbfac8fa4 100644 --- a/src/mapml/index.js +++ b/src/mapml/index.js @@ -80,6 +80,16 @@ import { featureIndexOverlay, FeatureIndexOverlay } from './layers/FeatureIndexOverlay'; +// element support +import { createLayerControlExtentHTML } from './elementSupport/extents/createLayerControlForExtent'; +import { createLayerControlHTML } from './elementSupport/layers/createLayerControlForLayer'; +import { ZoomInput } from './elementSupport/inputs/zoomInput'; +import { HiddenInput } from './elementSupport/inputs/hiddenInput'; +import { WidthInput } from './elementSupport/inputs/widthInput'; +import { HeightInput } from './elementSupport/inputs/heightInput'; +import { LocationInput } from './elementSupport/inputs/locationInput'; + +import { DOMTokenList } from './utils/DOMTokenList'; /* global L, Node */ (function (window, document, undefined) { @@ -805,6 +815,7 @@ import { M.axisToXY = Util.axisToXY; M.csToAxes = Util.csToAxes; M._convertAndFormatPCRS = Util._convertAndFormatPCRS; + M.extentToBounds = Util.extentToBounds; M.axisToCS = Util.axisToCS; M._parseNumber = Util._parseNumber; M._extractInputBounds = Util._extractInputBounds; @@ -900,4 +911,21 @@ import { M.Geometry = Geometry; M.geometry = geometry; + + // element support + M._createLayerControlExtentHTML = createLayerControlExtentHTML; + M._createLayerControlHTML = createLayerControlHTML; + M.ZoomInput = ZoomInput; + M.HiddenInput = HiddenInput; + M.WidthInput = WidthInput; + M.HeightInput = HeightInput; + M.LocationInput = LocationInput; + + // constants + M.TILE_SIZE = 256; + M.FALLBACK_PROJECTION = 'OSMTILE'; + M.FALLBACK_CS = 'TILEMATRIX'; + M.BLANK_TT_TREF = 'mapmltemplatedtileplaceholder'; + + M.DOMTokenList = DOMTokenList; })(window, document); diff --git a/src/mapml/layers/DebugOverlay.js b/src/mapml/layers/DebugOverlay.js index 2c089135b..049eab823 100644 --- a/src/mapml/layers/DebugOverlay.js +++ b/src/mapml/layers/DebugOverlay.js @@ -199,7 +199,8 @@ export var DebugVectors = L.LayerGroup.extend({ map.options.crs.scale(0) ); this._centerVector = L.circle(map.options.crs.pointToLatLng(center, 0), { - radius: 250 + radius: 250, + className: 'mapml-debug-vectors projection-centre' }); this._centerVector.bindTooltip('Projection Center'); @@ -210,63 +211,92 @@ export var DebugVectors = L.LayerGroup.extend({ }, _addBounds: function (map) { - let id = Object.keys(map._layers), - layers = map._layers, - colors = ['#FF5733', '#8DFF33', '#3397FF', '#E433FF', '#F3FF33'], - j = 0; - - this.addLayer(this._centerVector); - - for (let i of id) { - if (layers[i].layerBounds || layers[i].extentBounds) { - let boundsArray; - if (layers[i].layerBounds) { - boundsArray = [ - layers[i].layerBounds.min, - L.point(layers[i].layerBounds.max.x, layers[i].layerBounds.min.y), - layers[i].layerBounds.max, - L.point(layers[i].layerBounds.min.x, layers[i].layerBounds.max.y) - ]; - } else { - boundsArray = [ - layers[i].extentBounds.min, - L.point(layers[i].extentBounds.max.x, layers[i].extentBounds.min.y), - layers[i].extentBounds.max, - L.point(layers[i].extentBounds.min.x, layers[i].extentBounds.max.y) - ]; - } - let boundsRect = projectedExtent(boundsArray, { - color: colors[j % colors.length], - weight: 2, - opacity: 1, - fillOpacity: 0.01, - fill: true - }); - if (layers[i].options._leafletLayer) - boundsRect.bindTooltip(layers[i].options._leafletLayer._title, { - sticky: true + // to delay the addBounds to wait for the layer.extentbounds / layer.layerbounds to be ready when the layer- checked attribute is changed + setTimeout(() => { + let id = Object.keys(map._layers), + layers = map._layers, + colors = ['#FF5733', '#8DFF33', '#3397FF', '#E433FF', '#F3FF33'], + j = 0; + + this.addLayer(this._centerVector); + + for (let i of id) { + if (layers[i].layerBounds || layers[i].extentBounds) { + let boundsArray; + if (layers[i].layerBounds) { + boundsArray = [ + layers[i].layerBounds.min, + L.point(layers[i].layerBounds.max.x, layers[i].layerBounds.min.y), + layers[i].layerBounds.max, + L.point(layers[i].layerBounds.min.x, layers[i].layerBounds.max.y) + ]; + } else { + boundsArray = [ + layers[i].extentBounds.min, + L.point( + layers[i].extentBounds.max.x, + layers[i].extentBounds.min.y + ), + layers[i].extentBounds.max, + L.point( + layers[i].extentBounds.min.x, + layers[i].extentBounds.max.y + ) + ]; + } + + // boundsTestTag adds the value of from the element + // if it exists. this simplifies debugging because the svg path will be + // tagged with the layer it came from + let boundsTestTag = + layers[i].extentBounds && + layers[i].options.extentEl.parentLayer.hasAttribute('data-testid') + ? layers[i].options.extentEl.parentLayer.getAttribute( + 'data-testid' + ) + : layers[i].layerBounds && + layers[i].options._leafletLayer._layerEl.hasAttribute( + 'data-testid' + ) + ? layers[i].options._leafletLayer._layerEl.getAttribute( + 'data-testid' + ) + : ''; + let boundsRect = projectedExtent(boundsArray, { + className: this.options.className.concat(' ', boundsTestTag), + color: colors[j % colors.length], + weight: 2, + opacity: 1, + fillOpacity: 0.01, + fill: true }); - this.addLayer(boundsRect); - j++; + if (layers[i].options._leafletLayer) + boundsRect.bindTooltip(layers[i].options._leafletLayer._title, { + sticky: true + }); + this.addLayer(boundsRect); + j++; + } } - } - if (map.totalLayerBounds) { - let totalBoundsArray = [ - map.totalLayerBounds.min, - L.point(map.totalLayerBounds.max.x, map.totalLayerBounds.min.y), - map.totalLayerBounds.max, - L.point(map.totalLayerBounds.min.x, map.totalLayerBounds.max.y) - ]; - - let totalBounds = projectedExtent(totalBoundsArray, { - color: '#808080', - weight: 5, - opacity: 0.5, - fill: false - }); - this.addLayer(totalBounds); - } + if (map.totalLayerBounds) { + let totalBoundsArray = [ + map.totalLayerBounds.min, + L.point(map.totalLayerBounds.max.x, map.totalLayerBounds.min.y), + map.totalLayerBounds.max, + L.point(map.totalLayerBounds.min.x, map.totalLayerBounds.max.y) + ]; + + let totalBounds = projectedExtent(totalBoundsArray, { + className: 'mapml-debug-vectors mapml-total-bounds', + color: '#808080', + weight: 5, + opacity: 0.5, + fill: false + }); + this.addLayer(totalBounds); + } + }, 0); }, _mapLayerUpdate: function (e) { diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index f37d90942..1fc30d0a9 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -1,5 +1,3 @@ -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. diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 0287021c6..809b76f62 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1,7 +1,3 @@ -/* global M */ - -import { FALLBACK_PROJECTION, BLANK_TT_TREF } from '../utils/Constants'; - export var MapMLLayer = L.Layer.extend({ // zIndex has to be set, for the case where the layer is added to the // map before the layercontrol is used to control it (where autoZindex is used) @@ -53,7 +49,6 @@ export var MapMLLayer = L.Layer.extend({ // established by metadata in the content, we should use map properties // to set the extent, but the map won't be available until the // element is attached to the element, wait for that to happen. - this.on('attached', this._validateExtent, this); // weirdness. options is actually undefined here, despite the hardcoded // options above. If you use this.options, you see the options defined // above. Not going to change this, but failing to understand ATM. @@ -78,56 +73,11 @@ export var MapMLLayer = L.Layer.extend({ this._container.style.zIndex = this.options.zIndex; } }, - // remove all the extents before removing the layer from the map - _removeExtents: function (map) { - 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._properties._queries) { - delete this._properties._queries; - } - }, - _changeOpacity: function (e) { - if (e && e.target && e.target.value >= 0 && e.target.value <= 1.0) { - this.changeOpacity(e.target.value); - } - }, changeOpacity: function (opacity) { this._container.style.opacity = opacity; this._layerEl._opacity = opacity; - if (this.opacityEl) this.opacityEl.value = opacity; - }, - _changeExtentOpacity: function (e) { - if (e && e.target && e.target.value >= 0 && e.target.value <= 1.0) { - this.templatedLayer.changeOpacity(e.target.value); - this._templateVars.opacity = e.target.value; - } - }, - _changeExtent: function (e, extentEl) { - if (e.target.checked) { - extentEl.checked = true; - if (this._layerEl.checked) { - extentEl.templatedLayer = M.templatedLayer(extentEl._templateVars, { - pane: this._container, - opacity: extentEl._templateVars.opacity, - _leafletLayer: this, - crs: extentEl.crs, - extentZIndex: extentEl.extentZIndex, - extentEl: extentEl._DOMnode || extentEl - }).addTo(this._map); - extentEl.templatedLayer.setZIndex(); - this._setLayerElExtent(); - } - } else { - L.DomEvent.stopPropagation(e); - extentEl.checked = false; - if (this._layerEl.checked) this._map.removeLayer(extentEl.templatedLayer); - this._setLayerElExtent(); - } + if (this._layerEl._opacitySlider) + this._layerEl._opacitySlider.value = opacity; }, titleIsReadOnly() { return !!this._titleIsReadOnly; @@ -138,8 +88,9 @@ export var MapMLLayer = L.Layer.extend({ // can be used if (!this.titleIsReadOnly()) { this._title = newName; - this._mapmlLayerItem.querySelector('.mapml-layer-item-name').innerHTML = - newName; + this._layerEl._layerControlHTML.querySelector( + '.mapml-layer-item-name' + ).innerHTML = newName; } }, getName() { @@ -147,6 +98,7 @@ export var MapMLLayer = L.Layer.extend({ }, onAdd: function (map) { + // probably don't need it except for layer context menu usage if (this._properties && !this._validProjection(map)) { this.validProjection = false; return; @@ -166,95 +118,13 @@ export var MapMLLayer = L.Layer.extend({ map.addLayer(this._staticTileLayer); } - const createAndAdd = createAndAddTemplatedLayers.bind(this); - // if the extent has been initialized and received, update the map, - if ( - this._properties && - this._properties._mapExtents && - this._properties._mapExtents[0]._templateVars - ) { - createAndAdd(); - } - this._setLayerElExtent(); - this.setZIndex(this.options.zIndex); this.getPane().appendChild(this._container); - setTimeout(() => { - map.fire('checkdisabled'); - }, 0); map.on('popupopen', this._attachSkipButtons, this); - - function createAndAddTemplatedLayers() { - if (this._properties && this._properties._mapExtents) { - for (let i = 0; i < this._properties._mapExtents.length; i++) { - if ( - this._properties._mapExtents[i]._templateVars && - this._properties._mapExtents[i].checked - ) { - if (!this._properties._mapExtents[i].extentZIndex) - this._properties._mapExtents[i].extentZIndex = i; - this._templatedLayer = M.templatedLayer( - this._properties._mapExtents[i]._templateVars, - { - pane: this._container, - opacity: this._properties._mapExtents[i]._templateVars.opacity, - _leafletLayer: this, - 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._properties._mapExtents[i] refers to the in remote mapml - extentEl: - this._properties._mapExtents[i]._DOMnode || - this._properties._mapExtents[i] - } - ).addTo(map); - this._properties._mapExtents[i].templatedLayer = - this._templatedLayer; - if (this._templatedLayer._queries) { - if (!this._properties._queries) this._properties._queries = []; - this._properties._queries = this._properties._queries.concat( - this._templatedLayer._queries - ); - } - } - if (this._properties._mapExtents[i].hasAttribute('opacity')) { - let opacity = - this._properties._mapExtents[i].getAttribute('opacity'); - this._properties._mapExtents[i].templatedLayer.changeOpacity( - opacity - ); - } - } - } - } - }, - - _validProjection: function (map) { - let noLayer = false; - 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 - ) { - noLayer = true; // if there's a single template where projections don't match, set noLayer to true - break; - } - } - } - } - return !(noLayer || this.getProjection() !== map.options.projection); }, - //sets the elements .bounds property - _setLayerElExtent: function () { + _calculateBounds: function () { let bounds, - zoomMax, - zoomMin, - maxNativeZoom, - minNativeZoom, zoomBounds = { minZoom: 0, maxZoom: 0, @@ -267,140 +137,107 @@ export var MapMLLayer = L.Layer.extend({ '_mapmlvectors', '_templatedLayer' ]; + const mapExtents = this._layerEl.querySelectorAll('map-extent').length + ? this._layerEl.querySelectorAll('map-extent') + : this._layerEl.shadowRoot + ? this._layerEl.shadowRoot.querySelectorAll('map-extent') + : []; layerTypes.forEach((type) => { - if (this[type]) { - if (type === '_templatedLayer') { - for (let i = 0; i < this._properties._mapExtents.length; i++) { - for ( - let j = 0; - j < this._properties._mapExtents[i]._templateVars.length; - j++ - ) { - let inputData = M._extractInputBounds( - this._properties._mapExtents[i]._templateVars[j] - ); - this._properties._mapExtents[i]._templateVars[ - j - ].tempExtentBounds = inputData.bounds; - this._properties._mapExtents[i]._templateVars[ - j - ].extentZoomBounds = inputData.zoomBounds; - } - } - for (let i = 0; i < this._properties._mapExtents.length; i++) { - if (this._properties._mapExtents[i].checked) { - for ( - let j = 0; - j < this._properties._mapExtents[i]._templateVars.length; - j++ - ) { - if (!bounds) { - bounds = - this._properties._mapExtents[i]._templateVars[j] - .tempExtentBounds; - zoomMax = - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.maxZoom; - zoomMin = - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.minZoom; - maxNativeZoom = - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.maxNativeZoom; - minNativeZoom = - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.minNativeZoom; - } else { - bounds.extend( - this._properties._mapExtents[i]._templateVars[j] - .tempExtentBounds.min - ); - bounds.extend( - this._properties._mapExtents[i]._templateVars[j] - .tempExtentBounds.max - ); - zoomMax = Math.max( - zoomMax, - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.maxZoom - ); - zoomMin = Math.min( - zoomMin, - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.minZoom - ); - maxNativeZoom = Math.max( - maxNativeZoom, - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.maxNativeZoom - ); - minNativeZoom = Math.min( - minNativeZoom, - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.minNativeZoom - ); - } - } - } - } - zoomBounds.minZoom = zoomMin; - zoomBounds.maxZoom = zoomMax; - zoomBounds.minNativeZoom = minNativeZoom; - zoomBounds.maxNativeZoom = maxNativeZoom; - this._properties.zoomBounds = zoomBounds; - this._properties.layerBounds = bounds; - // assign each template the layer and zoom bounds - 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 if (type === '_staticTileLayer') { - if (this[type].layerBounds) { + if (type === '_templatedLayer' && mapExtents.length) { + let zoomMax = zoomBounds.maxZoom, + zoomMin = zoomBounds.minZoom, + maxNativeZoom = zoomBounds.maxNativeZoom, + minNativeZoom = zoomBounds.minNativeZoom; + for (let i = 0; i < mapExtents.length; i++) { + if (mapExtents[i]._templatedLayer.bounds) { + let templatedLayer = mapExtents[i]._templatedLayer; if (!bounds) { - bounds = this[type].layerBounds; - zoomBounds = this[type].zoomBounds; + bounds = templatedLayer.bounds; + zoomBounds = templatedLayer.zoomBounds; } else { - bounds.extend(this[type].layerBounds.min); - bounds.extend(this[type].layerBounds.max); + bounds.extend(templatedLayer.bounds.min); + bounds.extend(templatedLayer.bounds.max); + zoomMax = Math.max(zoomMax, templatedLayer.zoomBounds.maxZoom); + zoomMin = Math.min(zoomMin, templatedLayer.zoomBounds.minZoom); + maxNativeZoom = Math.max( + maxNativeZoom, + templatedLayer.zoomBounds.maxNativeZoom + ); + minNativeZoom = Math.min( + minNativeZoom, + templatedLayer.zoomBounds.minNativeZoom + ); + zoomBounds.minZoom = zoomMin; + zoomBounds.maxZoom = zoomMax; + zoomBounds.minNativeZoom = minNativeZoom; + zoomBounds.maxNativeZoom = maxNativeZoom; } } - } 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 (type === '_staticTileLayer' && this._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 === '_mapmlvectors') { - 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' && this._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' && + this._mapmlvectors && + Object.keys(this[type]._layers).length !== 0 + ) { + 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); } } } }); 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._properties.crs, - this._properties.projection - ), - { zoom: zoomBounds } - ); + this.bounds = bounds; + this.zoomBounds = zoomBounds; } }, + _validProjection: function (map) { + const mapExtents = this._layerEl.querySelectorAll('map-extent'); + let noLayer = false; + if (this._properties && mapExtents.length > 0) { + for (let i = 0; i < mapExtents.length; i++) { + if (mapExtents[i]._templateVars) { + for (let template of mapExtents[i]._templateVars) + if ( + !template.projectionMatch && + template.projection !== map.options.projection + ) { + noLayer = true; // if there's a single template where projections don't match, set noLayer to true + break; + } + } + } + } + return !(noLayer || this.getProjection() !== map.options.projection); + }, + addTo: function (map) { map.addLayer(this); return this; @@ -408,16 +245,6 @@ export var MapMLLayer = L.Layer.extend({ getEvents: function () { return { zoomanim: this._onZoomAnim }; }, - redraw: function () { - // for now, only redraw templated layers. - 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(); - } - } - } - }, _onZoomAnim: function (e) { // this callback will be invoked AFTER has been removed // but due to the characteristic of JavaScript, the context (this pointer) can still be used @@ -426,12 +253,15 @@ export var MapMLLayer = L.Layer.extend({ return; } // get the min and max zooms from all extents + const layerEl = this._layerEl, + // prerequisite: no inline and remote mapml elements exists at the same time + mapExtents = layerEl.shadowRoot + ? layerEl.shadowRoot.querySelectorAll('map-extent') + : layerEl.querySelectorAll('map-extent'); var toZoom = e.zoom, zoom = - this._properties && this._properties._mapExtents - ? this._properties._mapExtents[0].querySelector( - 'map-input[type=zoom]' - ) + mapExtents.length > 0 + ? mapExtents[0].querySelector('map-input[type=zoom]') : null, min = zoom && zoom.hasAttribute('min') @@ -442,10 +272,8 @@ export var MapMLLayer = L.Layer.extend({ ? parseInt(zoom.getAttribute('max')) : this._map.getMaxZoom(); if (zoom) { - for (let i = 1; i < this._properties._mapExtents.length; i++) { - zoom = this._properties._mapExtents[i].querySelector( - 'map-input[type=zoom]' - ); + for (let i = 1; i < mapExtents.length; i++) { + zoom = mapExtents[i].querySelector('map-input[type=zoom]'); if (zoom && zoom.hasAttribute('min')) { min = Math.min(parseInt(zoom.getAttribute('min')), min); } @@ -474,369 +302,31 @@ export var MapMLLayer = L.Layer.extend({ this._layerEl.src = this._properties.zoomout; } } - if (this._templatedLayer && canZoom) { - // get the new extent - //this._initExtent(); - } }, onRemove: function (map) { L.DomUtil.remove(this._container); if (this._staticTileLayer) map.removeLayer(this._staticTileLayer); if (this._mapmlvectors) map.removeLayer(this._mapmlvectors); if (this._imageLayer) map.removeLayer(this._imageLayer); - if (this._properties && this._properties._mapExtents) - this._removeExtents(map); - - map.fire('checkdisabled'); map.off('popupopen', this._attachSkipButtons); }, getAttribution: function () { return this.options.attribution; }, - getLayerUserControlsHTML: function () { - return this._mapmlLayerItem - ? this._mapmlLayerItem - : this._createLayerControlHTML(); + getBase: function () { + return new URL( + this._content.querySelector('map-base') + ? this._content.querySelector('map-base').getAttribute('href') + : this._content.nodeName === 'LAYER-' + ? this._content.baseURI + : this._href, + this._href + ).href; }, - _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); - - layerItemSettings.hidden = true; - settingsButtonNameIcon.setAttribute('aria-hidden', true); - - 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 ( - e.target.closest('fieldset').nextElementSibling && - !e.target.closest('fieldset').nextElementSibling.disbaled - ) { - 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 - ); - - 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'); - - 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); - } - }; - - 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); - - itemToggleLabel.appendChild(input); - itemToggleLabel.appendChild(layerItemName); - itemSettingControlButton.appendChild(settingsButtonNameIcon); - settingsButtonNameIcon.appendChild(svgSettingsControlIcon); - - if (this._styles) { - layerItemSettings.appendChild(this._styles); - } - - 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); - } - - // 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); - } - } - return this._mapmlLayerItem; + addExtentToLayerControl: function (contents) { + this._layerEl._propertiesGroupAnatomy.appendChild(contents); + // remove hidden attribute, if it exists + this._layerEl._propertiesGroupAnatomy.removeAttribute('hidden'); }, _initialize: function (content) { if (!this._href && !content) { @@ -849,21 +339,12 @@ export var MapMLLayer = L.Layer.extend({ // referred to by this._content), we should use that content. _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; + var base = layer.getBase(); 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(); @@ -872,7 +353,6 @@ export var MapMLLayer = L.Layer.extend({ 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) { @@ -913,9 +393,7 @@ export var MapMLLayer = L.Layer.extend({ ); } layer._properties.projection = projection; - if (layer._properties.projection === layer.options.mapprojection) { - layer._properties.crs = M[layer._properties.projection]; - } + 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 @@ -953,528 +431,6 @@ export var MapMLLayer = L.Layer.extend({ } 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 ( - extents[j].querySelector( - 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' - ) - ) { - 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); - } - } - } - 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; - } - - // 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; - 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 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 = 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; - - 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 ( - (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'); - - 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); - } - }; - - 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; - }; - } - }; - return extent; - } - 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; - - extentFallback.zoom = content.zoom || extentFallback.zoom; - - 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; - } - - 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')}}`; - } - } - - 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; - } - } - if ( - (template && vcount.length === inputs.length) || - template === BLANK_TT_TREF - ) { - 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 - }); - } - } - 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]) - ); - } - - 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]) - ); - } - - 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]'); @@ -1579,7 +535,11 @@ export var MapMLLayer = L.Layer.extend({ 'id', 'rad-' + L.stamp(styleOptionInput) ); - styleOptionInput.setAttribute('name', 'styles-' + layer._title); + styleOptionInput.setAttribute( + 'name', + // grouping radio buttons based on parent layer's style + 'styles-' + L.stamp(stylesControl) + ); styleOptionInput.setAttribute( 'value', styleLinks[j].getAttribute('title') @@ -1619,9 +579,6 @@ export var MapMLLayer = L.Layer.extend({ } 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(); } function copyRemoteContentToShadowRoot() { // only run when content is loaded from network, puts features etc @@ -1630,7 +587,12 @@ export var MapMLLayer = L.Layer.extend({ return; } let shadowRoot = layer._layerEl.shadowRoot; - let elements = mapml.children[0].children[1].children; + // get the map-meta[name=projection/cs/extent/zoom] from map-head of remote mapml, attach them to the shadowroot + let headMeta = + mapml.children[0].children[0].querySelectorAll('map-meta[name]'); + // get the elements inside map-body of remote mapml + let bodyElements = mapml.children[0].children[1].children; + let elements = [...headMeta, ...bodyElements]; if (elements) { let baseURL = mapml.children[0].children[0] .querySelector('map-base') @@ -1694,44 +656,6 @@ export var MapMLLayer = L.Layer.extend({ } } }, - _validateExtent: function () { - // TODO: change so that the _extent bounds are set based on inputs - if (!this._properties || !this._map) { - return; - } - var serverExtent = this._properties._mapExtents - ? this._properties._mapExtents - : [this._properties], - lp; - - // loop through the map-extent elements and assign each one its crs - for (let i = 0; i < serverExtent.length; i++) { - if (!serverExtent[i].querySelector) { - return; - } - if ( - serverExtent[i].querySelector( - '[type=zoom][min=""], [type=zoom][max=""]' - ) - ) { - var zoom = serverExtent[i].querySelector('[type=zoom]'); - zoom.setAttribute('min', this._map.getMinZoom()); - zoom.setAttribute('max', this._map.getMaxZoom()); - } - lp = serverExtent[i].hasAttribute('units') - ? serverExtent[i].getAttribute('units') - : null; - if (lp && M[lp]) { - if (this._properties._mapExtents) - this._properties._mapExtents[i].crs = M[lp]; - else this._properties.crs = M[lp]; - } else { - if (this._properties._mapExtents) - this._properties._mapExtents[i].crs = M.OSMTILE; - else this._properties.crs = M.OSMTILE; - } - } - }, // new getProjection, maybe simpler, but doesn't work... getProjection: function () { if (!this._properties) { @@ -1740,25 +664,24 @@ export var MapMLLayer = L.Layer.extend({ return this._properties.projection; }, getQueryTemplates: function (pcrsClick) { + const mapExtents = this._layerEl.querySelectorAll('map-extent').length + ? this._layerEl.querySelectorAll('map-extent') + : this._layerEl.shadowRoot.querySelectorAll('map-extent'); if (this._properties && this._properties._queries) { var templates = []; // only return queries that are in bounds if ( this._layerEl.checked && !this._layerEl.hidden && - this._mapmlLayerItem + this._layerEl._layerControlHTML ) { - let layerAndExtents = this._mapmlLayerItem.querySelectorAll( + let layerAndExtents = this._layerEl._layerControlHTML.querySelectorAll( '.mapml-layer-item-name' ); for (let i = 0; i < layerAndExtents.length; i++) { - if ( - layerAndExtents[i].extent || - this._properties._mapExtents.length === 1 - ) { + if (layerAndExtents[i].extent || mapExtents.length === 1) { // the layer won't have an .extent property, this is kind of a hack - let extent = - layerAndExtents[i].extent || this._properties._mapExtents[0]; + let extent = layerAndExtents[i].extent || mapExtents[0]; for (let j = 0; j < extent._templateVars.length; j++) { if (extent.checked) { let template = extent._templateVars[j]; @@ -1805,7 +728,11 @@ export var MapMLLayer = L.Layer.extend({ // if the popup is for a static / templated feature, the "zoom to here" link can be attached once the popup opens attachZoomLink.call(popup); } else { - layer = popup._source._templatedLayer; + // getting access to the first map-extent to get access to _templatedLayer to use it's (possibly) generic _previousFeature + _nextFeature methods. + const mapExtent = + popup._source._layerEl.querySelector('map-extent') || + popup._source._layerEl.shadowRoot.querySelector('map-extent'); + layer = mapExtent._templatedLayer; // if the popup is for a query, the "zoom to here" link should be re-attached every time new pagination features are displayed map.on('attachZoomLink', attachZoomLink, popup); } diff --git a/src/mapml/layers/StaticTileLayer.js b/src/mapml/layers/StaticTileLayer.js index 91d66501b..fc9462ac1 100644 --- a/src/mapml/layers/StaticTileLayer.js +++ b/src/mapml/layers/StaticTileLayer.js @@ -1,11 +1,11 @@ export var StaticTileLayer = L.GridLayer.extend({ initialize: function (options) { + L.setOptions(this, options); this.zoomBounds = this._getZoomBounds( options.tileContainer, options.maxZoomBound ); - L.extend(options, this.zoomBounds); - L.setOptions(this, options); + L.extend(this.options, this.zoomBounds); this._groups = this._groupTiles( this.options.tileContainer.getElementsByTagName('map-tile') ); @@ -119,8 +119,11 @@ export var StaticTileLayer = L.GridLayer.extend({ _getZoomBounds: function (container, maxZoomBound) { if (!container) return null; + // should read zoom information from map-meta (of layer-) instead of the zoom attribute value of map-tile let meta = M._metaContentToObject( - container.getElementsByTagName('map-tiles')[0].getAttribute('zoom') + this.options._leafletLayer._layerEl + .querySelector('map-meta[name=zoom]') + .getAttribute('content') ), zoom = {}, tiles = container.getElementsByTagName('map-tile'); @@ -134,14 +137,11 @@ export var StaticTileLayer = L.GridLayer.extend({ zoom.maxNativeZoom = Math.max(zoom.maxNativeZoom, lZoom); } - //hard coded to only natively zoom out 2 levels, any more and too many tiles are going to be loaded in at one time - //lagging the users computer - zoom.minZoom = zoom.minNativeZoom - 2 <= 0 ? 0 : zoom.minNativeZoom - 2; - zoom.maxZoom = maxZoomBound; - if (meta.min) - zoom.minZoom = - +meta.min < zoom.minNativeZoom - 2 ? zoom.minNativeZoom - 2 : +meta.min; - if (meta.max) zoom.maxZoom = +meta.max; + // currently the min and max zoom bounds of staticTileLayer is set based on map-meta + // can be hard coded to only natively zoom out 2 levels, any more and too many tiles are going to be loaded in at one time + // it will avoid lagging the users computer, but it will also cause the bug that the initial zoom level of map is inproperly set + zoom.minZoom = +meta.min || 0; + zoom.maxZoom = +meta.max || maxZoomBound; return zoom; }, diff --git a/src/mapml/layers/TemplatedFeaturesLayer.js b/src/mapml/layers/TemplatedFeaturesLayer.js index ab4e74268..79a216d3f 100644 --- a/src/mapml/layers/TemplatedFeaturesLayer.js +++ b/src/mapml/layers/TemplatedFeaturesLayer.js @@ -7,7 +7,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ this.isVisible = true; this._template = template; this._extentEl = options.extentEl; - this._container = L.DomUtil.create('div', 'leaflet-layer', options.pane); + this._container = L.DomUtil.create('div', 'leaflet-layer'); L.extend(options, this.zoomBounds); L.DomUtil.addClass(this._container, 'mapml-features-container'); delete options.opacity; @@ -23,6 +23,8 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ return events; }, onAdd: function () { + // this causes the layer (this._features) to actually render... + this.options.pane.appendChild(this._container); this._map._addZoomLimit(this); var opacity = this.options.opacity || 1, container = this._container, @@ -49,10 +51,16 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ geometry.bindPopup(c, { autoClose: false, minWidth: 108 }); } }); + } else { + // if this._features exists add the layer back + this._map.addLayer(this._features); } map.fire('moveend'); // TODO: replace with moveend handler for layer and not entire map }, + onRemove: function () { + this._map.removeLayer(this._features); + }, redraw: function () { this._onMoveEnd(); }, @@ -92,9 +100,6 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ this._map.getCenter(), steppedZoom ); - let url = this._getfeaturesUrl(steppedZoom, scaleBounds); - //No request needed if the current template url is the same as the url to request - if (url === this._url) return; let mapBounds = M.pixelToPCRSBounds( this._map.getPixelBounds(), @@ -106,6 +111,12 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ mapZoom >= this.zoomBounds.minZoom && this.extentBounds.overlaps(mapBounds); + // should set this.isVisible properly BEFORE return, otherwise will cause layer-.validateDisabled not work properly + let url = this._getfeaturesUrl(steppedZoom, scaleBounds); + // No request needed if the current template url is the same as the url to request + if (url === this._url) return; + + // do cleaning up for new request this._features.clearLayers(); // shadow may has not yet attached to for the first-time rendering if (this._extentEl.shadowRoot) { @@ -220,9 +231,6 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ this._container.style.zIndex = this.options.zIndex; } }, - onRemove: function () { - this._map.removeLayer(this._features); - }, _getfeaturesUrl: function (zoom, bounds) { if (zoom === undefined) zoom = this._map.getZoom(); if (bounds === undefined) bounds = this._map.getPixelBounds(); diff --git a/src/mapml/layers/TemplatedImageLayer.js b/src/mapml/layers/TemplatedImageLayer.js index e98747160..734d53cd2 100644 --- a/src/mapml/layers/TemplatedImageLayer.js +++ b/src/mapml/layers/TemplatedImageLayer.js @@ -1,7 +1,7 @@ export var TemplatedImageLayer = L.Layer.extend({ initialize: function (template, options) { this._template = template; - this._container = L.DomUtil.create('div', 'leaflet-layer', options.pane); + this._container = L.DomUtil.create('div', 'leaflet-layer'); L.DomUtil.addClass(this._container, 'mapml-image-container'); let inputData = M._extractInputBounds(template); this.zoomBounds = inputData.zoomBounds; @@ -22,6 +22,7 @@ export var TemplatedImageLayer = L.Layer.extend({ return events; }, onAdd: function () { + this.options.pane.appendChild(this._container); this._map._addZoomLimit(this); //used to set the zoom limit of the map this.setZIndex(this.options.zIndex); this._onAdd(); @@ -159,9 +160,9 @@ export var TemplatedImageLayer = L.Layer.extend({ } }, onRemove: function (map) { + L.DomUtil.remove(this._container); this._clearLayer(); map._removeZoomLimit(this); - this._container = null; }, getImageUrl: function (pixelBounds, zoom) { var obj = {}; diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index 23923718f..b393d0152 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -2,8 +2,9 @@ export var TemplatedLayer = L.Layer.extend({ initialize: function (templates, options) { this._templates = templates; L.setOptions(this, options); - this._container = L.DomUtil.create('div', 'leaflet-layer', options.pane); - this._container.style.opacity = this.options.opacity; + this._container = L.DomUtil.create('div', 'leaflet-layer'); + this._extentEl = this.options.extentEl; + this.changeOpacity(this.options.opacity); L.DomUtil.addClass(this._container, 'mapml-templatedlayer-container'); for (var i = 0; i < templates.length; i++) { @@ -257,6 +258,8 @@ export var TemplatedLayer = L.Layer.extend({ } }, onAdd: function (map) { + // add to this.options.pane + this.options.pane.appendChild(this._container); for (var i = 0; i < this._templates.length; i++) { if (this._templates[i].rel !== 'query') { map.addLayer(this._templates[i].layer); @@ -308,6 +311,9 @@ export var TemplatedLayer = L.Layer.extend({ changeOpacity: function (opacity) { this._container.style.opacity = opacity; + this._extentEl._opacity = opacity; + if (this._extentEl._opacitySlider) + this._extentEl._opacitySlider.value = opacity; } }); export var templatedLayer = function (templates, options) { diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 6daea770d..c34b77698 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -1,5 +1,3 @@ -import { BLANK_TT_TREF } from '../utils/Constants'; - export var TemplatedTileLayer = L.TileLayer.extend({ // a TemplateTileLayer is similar to a L.TileLayer except its templates are // defined by the