diff --git a/Leaflet.fullscreen.js b/Leaflet.fullscreen.js new file mode 100644 index 000000000..2a4d13ba5 --- /dev/null +++ b/Leaflet.fullscreen.js @@ -0,0 +1,172 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['leaflet'], factory); + } else if (typeof module !== 'undefined') { + // Node/CommonJS + module.exports = factory(require('leaflet')); + } else { + // Browser globals + if (typeof window.L === 'undefined') { + throw new Error('Leaflet must be loaded first'); + } + factory(window.L); + } +}(function (L) { + L.Control.Fullscreen = L.Control.extend({ + options: { + position: 'topleft', + title: { + 'false': 'View Fullscreen', + 'true': 'Exit Fullscreen' + } + }, + + onAdd: function (map) { + var container = L.DomUtil.create('div', 'leaflet-control-fullscreen leaflet-bar leaflet-control'); + + this.link = L.DomUtil.create('a', 'leaflet-control-fullscreen-button leaflet-bar-part', container); + this.link.href = '#'; + + this._map = map; + this._map.on('fullscreenchange', this._toggleTitle, this); + this._toggleTitle(); + + L.DomEvent.on(this.link, 'click', this._click, this); + + return container; + }, + + onRemove: function (map) { + map.off('fullscreenchange', this._toggleTitle, this); + }, + + _click: function (e) { + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + this._map.toggleFullscreen(this.options); + }, + + _toggleTitle: function() { + this.link.title = this.options.title[this._map.isFullscreen()]; + } + }); + + L.Map.include({ + isFullscreen: function () { + return this._isFullscreen || false; + }, + + toggleFullscreen: function (options) { + var container = this.getContainer(); + if (this.isFullscreen()) { + if (options && options.pseudoFullscreen) { + this._disablePseudoFullscreen(container); + } else if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitCancelFullScreen) { + document.webkitCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } else { + this._disablePseudoFullscreen(container); + } + } else { + if (options && options.pseudoFullscreen) { + this._enablePseudoFullscreen(container); + } else if (container.requestFullscreen) { + container.requestFullscreen(); + } else if (container.mozRequestFullScreen) { + container.mozRequestFullScreen(); + } else if (container.webkitRequestFullscreen) { + container.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (container.msRequestFullscreen) { + container.msRequestFullscreen(); + } else { + this._enablePseudoFullscreen(container); + } + } + + }, + + _enablePseudoFullscreen: function (container) { + L.DomUtil.addClass(container, 'leaflet-pseudo-fullscreen'); + this._setFullscreen(true); + this.fire('fullscreenchange'); + }, + + _disablePseudoFullscreen: function (container) { + L.DomUtil.removeClass(container, 'leaflet-pseudo-fullscreen'); + this._setFullscreen(false); + this.fire('fullscreenchange'); + }, + + _setFullscreen: function(fullscreen) { + this._isFullscreen = fullscreen; + var container = this.getContainer(); + if (fullscreen) { + L.DomUtil.addClass(container, 'leaflet-fullscreen-on'); + } else { + L.DomUtil.removeClass(container, 'leaflet-fullscreen-on'); + } + this.invalidateSize(); + }, + + _onFullscreenChange: function (e) { + var fullscreenElement = + document.fullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + document.msFullscreenElement; + + if ( !this._isFullscreen) { + this._setFullscreen(true); + this.fire('fullscreenchange'); + } else if ( this._isFullscreen) { + this._setFullscreen(false); + this.fire('fullscreenchange'); + } + } + }); + + L.Map.mergeOptions({ + fullscreenControl: false + }); + + L.Map.addInitHook(function () { + if (this.options.fullscreenControl) { + this.fullscreenControl = new L.Control.Fullscreen(this.options.fullscreenControl); + this.addControl(this.fullscreenControl); + } + + var fullscreenchange; + + if ('onfullscreenchange' in document) { + fullscreenchange = 'fullscreenchange'; + } else if ('onmozfullscreenchange' in document) { + fullscreenchange = 'mozfullscreenchange'; + } else if ('onwebkitfullscreenchange' in document) { + fullscreenchange = 'webkitfullscreenchange'; + } else if ('onmsfullscreenchange' in document) { + fullscreenchange = 'MSFullscreenChange'; + } + + if (fullscreenchange) { + var onFullscreenChange = L.bind(this._onFullscreenChange, this); + + this.whenReady(function () { + L.DomEvent.on(document, fullscreenchange, onFullscreenChange); + }); + + this.on('unload', function () { + L.DomEvent.off(document, fullscreenchange, onFullscreenChange); + }); + } + }); + + L.control.fullscreen = function (options) { + return new L.Control.Fullscreen(options); + }; +})); diff --git a/README.md b/README.md index 0225c50e9..428bfc4b3 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,29 @@ -# web-map Customized Built-In <map> Element +# Customized built-in <map> element -[![Build Status](https://travis-ci.org/prushforth/Web-Map-Custom-Element.svg?branch=master)](https://travis-ci.org/prushforth/Web-Map-Custom-Element) [![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/Maps4HTML/Web-Map-Custom-Element) +![Build Status](https://api.travis-ci.com/Maps4HTML/Web-Map-Custom-Element.svg?branch=master) -The Customized Built-In <map> Element is a prototype [implementation](http://maps4html.github.io/Web-Map-Custom-Element/) of the [HTML-Map-Element specification](http://maps4html.github.io/HTML-Map-Element/spec/). +The customized built-in `` element is a prototype [implementation](http://maps4html.github.io/Web-Map-Custom-Element/) +of the [HTML-Map-Element specification](http://maps4html.github.io/HTML-Map-Element/spec/). -The HTML author can add MapML sources/layers by one or more the <`layer- src="..."`> elements as children of <`map`>. The map provides a default set of controls which are turned on or off with the map@controls boolean attribute. The @width and @height of the map should be specified either as attributes or via CSS rules. The initial zoom and location of the map are controlled by the @zoom and @lat, @lon attributes. The default projection is Web Mercator (OSMTILE). +The HTML author can add [MapML](https://maps4html.org/MapML/spec/) +sources/layers by specifying one or more `` elements as children of ``. +The map provides a default set of controls which are turned on or off with the map's `controls` boolean attribute. +The `width` and `height` attributes of the map should be specified, and can be overriden using CSS properties. +The initial zoom and location of the map are controlled by the `zoom`, `lat` and `lon` attributes. +The default `projection` is `OSMTILE` (Web Mercator). Example: - + ```html - + ``` -## Maps4HTML Community Group +## Maps for HTML Community Group -MapML and the web-map custom element area being developed by the W3C [Maps For HTML Community Group](http://www.w3.org/community/maps4html/). Membership in that group is encouraged, however you do not have to join to use the information found here. However, if you wish to contribute, please join the Maps For HTML Community Group, and help us make the Web a map-friendly platform for everyone, everywhere! +MapML and the <map> custom element are being developed by the W3C [Maps for HTML Community Group](http://www.w3.org/community/maps4html/). +Membership in the group is encouraged, however you do not have to join to use the information found here. +If you wish to contribute, please join the Maps For HTML Community Group, +and help us make the Web a map-friendly platform for everyone, everywhere! diff --git a/index.html b/index.html index b175b95fb..4f96abd9b 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,62 @@ - + - index-map.html - + + + index.html - + - - - + + + diff --git a/layer.js b/layer.js index 50cd97575..f58488231 100644 --- a/layer.js +++ b/layer.js @@ -91,8 +91,6 @@ export class MapLayer extends HTMLElement { } } connectedCallback() { - // this avoids displaying inline mapml content, such as features and inputs - this.style = "display: none"; this._ready(); // if the map has been attached, set this layer up wrt Leaflet map if (this.parentNode._map) { @@ -142,6 +140,16 @@ export class MapLayer extends HTMLElement { if (this._layerControl) { this._layerControl.addOrUpdateOverlay(this._layer, this.label); } + if (!this._layer.error) { + // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied + // to MapML extent as metadata + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event + this.dispatchEvent(new CustomEvent('loadedmetadata', {detail: + {target: this}})); + } else { + this.dispatchEvent(new CustomEvent('error', {detail: + {target: this}})); + } } _validateDisabled() { var layer = this._layer, map = layer._map; @@ -206,6 +214,7 @@ export class MapLayer extends HTMLElement { 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 diff --git a/leaflet.fullscreen.css b/leaflet.fullscreen.css new file mode 100644 index 000000000..276ef16c7 --- /dev/null +++ b/leaflet.fullscreen.css @@ -0,0 +1,33 @@ +.leaflet-control-fullscreen a { + background:#fff no-repeat 0 0; + background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg version='1.1' viewBox='0 0 26 52' width='26' height='52' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg display='none'%3E%3Crect width='26' height='52' color='%23000000' display='inline' fill='%23f2f2f2'/%3E%3Crect y='26' width='26' height='26' color='%23000000' display='inline' fill='%23e6e6e6'/%3E%3C/g%3E%3Cg transform='translate(0 -1000.4)'%3E%3Cuse transform='translate(0,26)' width='100%25' height='100%25' xlink:href='%23rect15634'/%3E%3Cuse transform='translate(0,26)' width='100%25' height='100%25' xlink:href='%23path15639'/%3E%3Cuse transform='translate(0,26)' width='100%25' height='100%25' xlink:href='%23path16061'/%3E%3Cuse transform='translate(0,26)' width='100%25' height='100%25' xlink:href='%23path16059'/%3E%3Cpath id='rect15634' transform='translate(0 1000.4)' d='m5 15v6h6v-2h-4v-4z' color='%23000000' fill='%23404040'/%3E%3Cpath id='path15639' transform='translate(0 1000.4)' d='m21 15v6h-6v-2h4v-4z' color='%23000000' fill='%23404040'/%3E%3Cpath d='m10 1037.4v4l1 1h4l1-1v-4l-1-1h-4z' color='%23000000' fill='%23404040'/%3E%3Cpath id='path16059' d='m5 1011.4v-6h6v2h-4v4z' color='%23000000' fill='%23404040'/%3E%3Cpath id='path16061' d='m21 1011.4v-6h-6v2h4v4z' color='%23000000' fill='%23404040'/%3E%3C/g%3E%3C/svg%3E%0A"); + background-size:26px 52px; + } + .leaflet-touch .leaflet-control-fullscreen a { + background-position: 2px 2px; + } + .leaflet-fullscreen-on .leaflet-control-fullscreen a { + background-position:0 -26px; + } + .leaflet-touch.leaflet-fullscreen-on .leaflet-control-fullscreen a { + background-position: 2px -24px; + } + +/* Do not combine these two rules; IE will break. */ +.leaflet-container:-webkit-full-screen { + width:100%!important; + height:100%!important; + } +.leaflet-container.leaflet-fullscreen-on { + width:100%!important; + height:100%!important; + } + +.leaflet-pseudo-fullscreen { + position:fixed!important; + width:100%!important; + height:100%!important; + top:0!important; + left:0!important; + z-index:99999; + } diff --git a/map-area.js b/map-area.js new file mode 100644 index 000000000..63e8c8f1c --- /dev/null +++ b/map-area.js @@ -0,0 +1,178 @@ +import './leaflet-src.js'; // a lightly modified version of Leaflet for use as browser module +import './proj4-src.js'; // modified version of proj4; could be stripped down for mapml +import './proj4leaflet.js'; // not modified, seems to adapt proj4 for leaflet use. +import './mapml.js'; // refactored URI usage, replaced with URL standard + +export class MapArea extends HTMLAreaElement { + static get observedAttributes() { + return ['coords','alt','href','shape','rel','type','target']; + } + // see comments below regarding attributeChangedCallback vs. getter/setter + // usage. Effectively, the user of the element must use the property, not + // the getAttribute/setAttribute/removeAttribute DOM API, because the latter + // calls don't result in the getter/setter being called (so you have to use + // the getter/setter directly) + get alt() { + return this.hasAttribute('alt') ? this.getAttribute('alt') : ""; + } + set alt(value) { + this.setAttribute('controls', value); + } + get coords() { + return this.hasAttribute('coords') ? this.getAttribute('coords') : ""; + } + set coords(coordinates) { + // what to do. Probably replace the feature with a new one, without changing + // anything else... + } + get href() { + return this.hasAttribute('href') ? this.getAttribute('href') : ""; + } + set href(url) { + this.href = url; + } + get shape() { + return this.hasAttribute('shape') ? this.getAttribute('shape') : "default"; + } + set shape(shape) { + shape = shape.toLowerCase(); + var re = /default|circle|rect|poly/; + if (shape.search(re)) { + this.shape = shape; + } + } + get rel() { + return this.hasAttribute('rel') ? this.getAttribute('rel') : ""; + } + set rel(rel) { + this.rel = rel; + } + get type() { + return this.hasAttribute('type') ? this.getAttribute('type') : ""; + } + set type(type) { + this.type = type; + } + get target() { + return this.hasAttribute('target') ? this.getAttribute('target') : ""; + } + constructor() { + // Always call super first in constructor + super(); + } + attributeChangedCallback(name, oldValue, newValue) { + + } + connectedCallback() { + // if the map has been attached, set this layer up wrt Leaflet map + if (this.parentElement._map) { + this._attachedToMap(); + } + } + _attachedToMap() { + // need the map to convert container points to LatLngs + this._map = this.parentElement._map; + var map = this.parentElement._map; + + // don't go through this if already done + if (!this._feature) { + // Scale this.coords if the this._map.poster exists because + // the img might have been scaled by CSS. + // compute the style properties to be applied to the feature + var options = this._styleToPathOptions(window.getComputedStyle(this)), + points = this.coords ? this._coordsToArray(this.coords): null; + // scale points if the poster exists because responsive areas + if (points && this.parentElement.poster) { + var worig = this.parentElement.poster.width, + wresp = this.parentElement.width, + wadjstmnt = (worig - wresp)/2, + horig = this.parentElement.poster.height, + hresp = this.parentElement.height, + hadjstmnt = (horig - hresp)/2; + for (var i = 0; i< points.length;i++) { + points[i][0] = points[i][0] - wadjstmnt; + points[i][1] = points[i][1] - hadjstmnt; + } + } + + if (this.shape === 'circle') { + var pixelRadius = parseInt(this.coords.split(",")[2]), + pointOnCirc = L.point(points[0]).add(L.point(0,pixelRadius)), + latLngOnCirc = map.containerPointToLatLng(pointOnCirc), + latLngCenter = map.containerPointToLatLng(points[0]), + radiusInMeters = map.distance(latLngCenter, latLngOnCirc); + this._feature = L.circle(latLngCenter, radiusInMeters, options).addTo(map); + } else if (!this.shape || this.shape === 'rect') { + var bounds = L.latLngBounds(map.containerPointToLatLng(points[0]), map.containerPointToLatLng(points[1])); + this._feature = L.rectangle(bounds, options).addTo(map); + } else if (this.shape === 'line') { + this._feature = L.polyline(this._pointsToLatLngs(points),options).addTo(map); + } else if (this.shape === 'poly') { + this._feature = L.polygon(this._pointsToLatLngs(points),options).addTo(map); + } else if (this.shape === 'default') { + // whole initial area of map is a hyperlink + this._feature = L.rectangle(map.getBounds(),options).addTo(map); + } + if (this.alt) { + // other Leaflet features are implemented via SVG. SVG displays tooltips + // based on the graphics child element. + var title = L.SVG.create('title'), + titleText = document.createTextNode(this.alt); + title.appendChild(titleText); + this._feature._path.appendChild(title); + } + if (this.href) { + // conditionally act on click on an area link. If no link it should be an + // inert area, but Leaflet doesn't quite support this. For a full + // implementation, we could actually use an image map replete with area + // children which would provide the linking / cursor change behaviours + // that are familiar to HTML authors versed in image maps. + this._feature.on('click', function() {if (this.href) {window.open(this.href);}}, this); + } + } + } + disconnectedCallback() { + this._map.removeLayer(this._feature); + delete this._feature; + } + _coordsToArray(containerPoints) { + // returns an array of arrays of coordinate pairs coordsToArray("1,2,3,4") -> [[1,2],[3,4]] + for (var i=1, points = [], coords = containerPoints.split(",");i= 0 && parsedVal <= 25)) { this.setAttribute('zoom', parsedVal); } - } } get layers() { return this.getElementsByTagName('layer-'); } + + get extent(){ + let map = this._map, + pcrsBounds = M.pixelToPCRSBounds( + map.getPixelBounds(), + map.getZoom(), + map.options.projection); + let formattedExtent = M.convertAndFormatPCRS(pcrsBounds, map); + if(map.getMaxZoom() !== Infinity){ + formattedExtent.zoom = { + minZoom:map.getMinZoom(), + maxZoom:map.getMaxZoom() + }; + } + return (formattedExtent); + } + constructor() { // Always call super first in constructor super(); + + this._source = this.outerHTML; let tmpl = document.createElement('template'); - tmpl.innerHTML = - `` + - ``; + tmpl.innerHTML = + `` + + `` + + ``; + let shadowRoot = this.attachShadow({mode: 'open'}); this._container = document.createElement('div'); - // you have to include this otherwise you have to use quirks mode, - // (by omitting the doctype), which is bad. - this._container.style.height = "100vh"; + + // Set default styles for the map element. + let mapDefaultCSS = document.createElement('style'); + mapDefaultCSS.innerHTML = + `:host {` + + `contain: content;` + // Contain layout and paint calculations within the map element. + `display: inline-block;` + // This together with dimension properties is required so that Leaflet isn't working with a height=0 box by default. + `overflow: hidden;` + // Make the map element behave and look more like a native element. + `height: 150px;` + // Provide a "default object size" (https://github.com/Maps4HTML/HTML-Map-Element/issues/31). + `width: 300px;` + + `border-width: 2px;` + + `border-style: inset;` + + `}`; + + // Hide all (light DOM) children of the map element. + let hideElementsCSS = document.createElement('style'); + hideElementsCSS.innerHTML = + `mapml-viewer > * {` + + `display: none!important;` + + `}`; + + shadowRoot.appendChild(mapDefaultCSS); shadowRoot.appendChild(tmpl.content.cloneNode(true)); shadowRoot.appendChild(this._container); - + + this.appendChild(hideElementsCSS); } connectedCallback() { -// console.log('Custom map element added to page.'); if (this.isConnected) { // the dimension attributes win, if they're there. A map does not - // have an intrinsic size, unlike an image or video, and so must + // have an intrinsic size, unlike an image or video, and so must // have a defined width and height. var s = window.getComputedStyle(this), - wpx = s.width, hpx=s.height, + wpx = s.width, hpx=s.height, w = parseInt(wpx.replace('px','')), h = parseInt(hpx.replace('px','')); @@ -102,18 +149,26 @@ export class GeoMap extends HTMLElement { } if (!this.height || this.height !== h) { - this._container.style.height = h; + this._container.style.height = hpx; this.height = h; } else { this._container.style.height = this.height+"px"; } + // create an array to track the history of the map and the current index + if(!this._history){ + this._history = []; + this._historyIndex = -1; + this._traversalCall = false; + } + // create the Leaflet map if this is the first time attached is called if (!this._map) { this._map = L.map(this._container, { center: new L.LatLng(this.lat, this.lon), projection: this.projection, query: true, + contextMenu: true, mapEl: this, crs: M[this.projection], zoom: this.zoom, @@ -121,18 +176,20 @@ export class GeoMap extends HTMLElement { // because the M.MapMLLayer invokes _tileLayer._onMoveEnd when // the mapml response is received the screen tends to flash. I'm sure // there is a better configuration than that, but at this moment - // I'm not sure how to approach that issue. - // See https://github.com/Maps4HTML/MapML-Leaflet-Client/issues/24 + // I'm not sure how to approach that issue. + // See https://github.com/Maps4HTML/MapML-Leaflet-Client/issues/24 fadeAnimation: true }); - // the attribution control is not optional - this._attributionControl = this._map.attributionControl.setPrefix('Maps4HTML | Leaflet'); + this._attributionControl = this._map.attributionControl.setPrefix('Maps4HTML | Leaflet'); // optionally add controls to the map if (this.controls) { this._layerControl = M.mapMlLayerControl(null,{"collapsed": true}).addTo(this._map); this._zoomControl = L.control.zoom().addTo(this._map); + if (!this.controlslist.toLowerCase().includes("nofullscreen")) { + this._fullScreenControl = L.control.fullscreen().addTo(this._map); + } } this._setUpEvents(); @@ -168,8 +225,20 @@ export class GeoMap extends HTMLElement { } _dropHandler(event) { event.preventDefault(); - // create a new child of this element - this.appendChild(new MapLayer(event.dataTransfer.getData("text"), 'Layer', true)); + // create a new child of this element + let l = new MapLayer(); + l.src = event.dataTransfer.getData("text"); + l.label = 'Layer'; + l.checked = 'true'; + this.appendChild(l); + l.addEventListener("error", function () { + if (l.parentElement) { + // should invoke lifecyle callbacks automatically by removing it from DOM + l.parentElement.removeChild(l); + } + // garbage collect it + l = null; + }); } _dragoverHandler(event) { function contains(list, value) { @@ -200,111 +269,115 @@ export class GeoMap extends HTMLElement { function () { this.dispatchEvent(new CustomEvent('load', {detail: {target: this}})); }, this); - this._map.on('preclick', + this._map.on('preclick', function (e) { - this.dispatchEvent(new CustomEvent('preclick', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, + this.dispatchEvent(new CustomEvent('preclick', {detail: + {lat: e.latlng.lat, lon: e.latlng.lng, x: e.containerPoint.x, y: e.containerPoint.y} })); }, this); - this._map.on('click', + this._map.on('click', function (e) { this.dispatchEvent(new CustomEvent('click', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, + {lat: e.latlng.lat, lon: e.latlng.lng, x: e.containerPoint.x, y: e.containerPoint.y} })); }, this); - this._map.on('dblclick', + this._map.on('dblclick', function (e) { this.dispatchEvent(new CustomEvent('dblclick', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, + {lat: e.latlng.lat, lon: e.latlng.lng, x: e.containerPoint.x, y: e.containerPoint.y} })); }, this); - this._map.on('mousemove', + this._map.on('mousemove', function (e) { this.dispatchEvent(new CustomEvent('mousemove', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, + {lat: e.latlng.lat, lon: e.latlng.lng, x: e.containerPoint.x, y: e.containerPoint.y} })); }, this); - this._map.on('mouseover', + this._map.on('mouseover', function (e) { this.dispatchEvent(new CustomEvent('mouseover', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, + {lat: e.latlng.lat, lon: e.latlng.lng, x: e.containerPoint.x, y: e.containerPoint.y} })); }, this); - this._map.on('mouseout', + this._map.on('mouseout', function (e) { this.dispatchEvent(new CustomEvent('mouseout', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, + {lat: e.latlng.lat, lon: e.latlng.lng, x: e.containerPoint.x, y: e.containerPoint.y} })); }, this); - this._map.on('mousedown', + this._map.on('mousedown', function (e) { this.dispatchEvent(new CustomEvent('mousedown', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, + {lat: e.latlng.lat, lon: e.latlng.lng, x: e.containerPoint.x, y: e.containerPoint.y} })); },this); - this._map.on('mouseup', + this._map.on('mouseup', function (e) { this.dispatchEvent(new CustomEvent('mouseup', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, + {lat: e.latlng.lat, lon: e.latlng.lng, x: e.containerPoint.x, y: e.containerPoint.y} })); }, this); - this._map.on('contextmenu', + this._map.on('contextmenu', function (e) { this.dispatchEvent(new CustomEvent('contextmenu', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, + {lat: e.latlng.lat, lon: e.latlng.lng, x: e.containerPoint.x, y: e.containerPoint.y} })); - }, this); - this._map.on('movestart', + }, this); + this._map.on('movestart', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('movestart', {detail: + this.dispatchEvent(new CustomEvent('movestart', {detail: {target: this}})); - }, this); - this._map.on('move', + }, this); + this._map.on('move', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('move', {detail: + this.dispatchEvent(new CustomEvent('move', {detail: {target: this}})); - }, this); - this._map.on('moveend', + }, this); + this._map.on('moveend', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('moveend', {detail: + this._addToHistory(); + this.dispatchEvent(new CustomEvent('moveend', {detail: {target: this}})); - }, this); - this._map.on('zoomstart', + }, this); + this._map.on('zoomstart', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('zoomstart', {detail: + this.dispatchEvent(new CustomEvent('zoomstart', {detail: {target: this}})); - }, this); - this._map.on('zoom', + }, this); + this._map.on('zoom', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('zoom', {detail: + this.dispatchEvent(new CustomEvent('zoom', {detail: {target: this}})); - }, this); - this._map.on('zoomend', + }, this); + this._map.on('zoomend', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('zoomend', {detail: + this.dispatchEvent(new CustomEvent('zoomend', {detail: {target: this}})); - }, this); + }, this); } _toggleControls(controls) { if (this._map) { if (controls && !this._layerControl) { this._zoomControl = L.control.zoom().addTo(this._map); this._layerControl = M.mapMlLayerControl(null,{"collapsed": true}).addTo(this._map); + if (!this.controlslist.toLowerCase().includes("nofullscreen")) { + this._fullScreenControl = L.control.fullscreen().addTo(this._map); + } for (var i=0;i 0){ + mapEl._historyIndex--; + } + let prev = history[mapEl._historyIndex]; + mapEl._traversalCall = true; + mapEl.zoomTo(prev.lat,prev.lng,prev.zoom); + } + + forward(){ + let mapEl = this, + history = this._history; + if(mapEl._historyIndex < history.length -1){ + mapEl._historyIndex++; + } + let next = history[this._historyIndex]; + mapEl._traversalCall = true; + mapEl.zoomTo(next.lat,next.lng,next.zoom); + } + + reload(){ + let mapEl = this, + initialLocation = mapEl._history.shift(); + mapEl._history = [initialLocation]; + mapEl._historyIndex = -1; + mapEl._traversalCall = true; + mapEl.zoomTo(initialLocation.lat,initialLocation.lng,initialLocation.zoom); + } + + viewSource(){ + let blob = new Blob([this._source],{type:"text/plain"}), + url = URL.createObjectURL(blob); + window.open(url); + URL.revokeObjectURL(url); + } + _ready() { // when used in a custom element, the leaflet script element is hidden inside // the import's shadow dom. @@ -368,25 +500,8 @@ export class GeoMap extends HTMLElement { } } }()); - if (this.hasAttribute('name')) { - var name = this.getAttribute('name'); - if (name) { - this.poster = document.querySelector('img[usemap='+'"#'+name+'"]'); - // firefox has an issue where the attribution control's use of - // _container.innerHTML does not work properly if the engine is throwing - // exceptions because there are no area element children of the image map - // for firefox only, a workaround is to actually remove the image... - if (this.poster) { - if (L.Browser.gecko) { - this.poster.removeAttribute('usemap'); - } - this._container.appendChild(this.poster); - } - } - } } } - // need to provide options { extends: ... } for custom built-in elements -window.customElements.define('geo-map', GeoMap); +window.customElements.define('mapml-viewer', MapViewer); window.customElements.define('layer-', MapLayer); diff --git a/mapml.css b/mapml.css index 95dbb763b..55695522b 100644 --- a/mapml.css +++ b/mapml.css @@ -1,3 +1,11 @@ +.leaflet-container { + /* Override the `background-color` set by leaflet.css, enables inheritance from + the map or element (same as iframes) to give the author more control. */ + background-color: transparent; + max-height: 100%; + max-width: 100%; +} + /* this is required by tiles which are actually divs with multiple images in them */ .leaflet-tile img { position: absolute; @@ -15,6 +23,9 @@ .leaflet-image-loaded { visibility: inherit; } +.leaflet-control-layers fieldset { + border: none; +} /* this is to indent the nested details in the layer control */ /* details/summary is in progress 2018-03-02 on ms-edge so gonna rely on that... https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/6261266-details-summary-elements */ @@ -36,6 +47,72 @@ .leaflet-control-layers details.mapml-control-layers > details.mapml-control-layers > span { display: block; } -.leaflet-control-layers details.mapml-control-layers > details.mapml-control-layers > span > label { +.leaflet-control-layers label { display: inline; } + +.leaflet-control-layers.leaflet-control { + margin-right: 10px; + margin-left: 10px; +} + +/* Disable dragging of controls. */ +.leaflet-control a { + -webkit-user-drag: none; +} + +/* Hide unintended highlighting of controls from clicking the map display in + quick succession. This is a workaround for `user-select: contain`, since it has + virtually no browser support: https://www.chromestatus.com/feature/5730263904550912. */ +.leaflet-control a::selection, +.leaflet-popup-close-button::selection, +.leaflet-control-attribution::selection { + background-color: transparent; +} + +/* Restore the default focus outline of UA stylesheets, + which Leaflet unfortunately removes (https://github.com/Leaflet/Leaflet/issues/6986). */ +.leaflet-container :focus { + outline-color: -webkit-focus-ring-color!important; + outline-style: auto!important; + outline-width: thin!important; + outline: revert!important; +} + +.mapml-contextmenu { + display: none; + max-width: 100%; + box-shadow: 0 1px 7px rgba(0,0,0,0.4); + -webkit-border-radius: 4px; + border-radius: 4px; + padding: 4px 0; + background-color: #fff; + cursor: default; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.mapml-contextmenu a.mapml-contextmenu-item { + display: block; + color: #222; + font-size: 12px; + line-height: 20px; + text-decoration: none; + padding: 0 12px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + cursor: default; +} + +.mapml-contextmenu a.mapml-contextmenu-item.over { + background-color: #f4f4f4; + border-top: 1px solid #f0f0f0; + border-bottom: 1px solid #f0f0f0; +} + +.mapml-contextmenu-separator { + border-bottom: 1px solid #ccc; + margin: 5px 0; +} \ No newline at end of file diff --git a/mapml.js b/mapml.js index 63456615f..63cb41a7a 100644 --- a/mapml.js +++ b/mapml.js @@ -1,3970 +1,4650 @@ -/* - * Copyright 2015-2016 Canada Centre for Mapping and Earth Observation, - * Earth Sciences Sector, Natural Resources Canada. - * - * License - * - * By obtaining and/or copying this work, you (the licensee) agree that you have - * read, understood, and will comply with the following terms and conditions. - * - * Permission to copy, modify, and distribute this work, with or without - * modification, for any purpose and without fee or royalty is hereby granted, - * provided that you include the following on ALL copies of the work or portions - * thereof, including modifications: - * - * The full text of this NOTICE in a location viewable to users of the - * redistributed or derivative work. - * - * Any pre-existing intellectual property disclaimers, notices, or terms and - * conditions. If none exist, the W3C Software and Document Short Notice should - * be included. - * - * Notice of any changes or modifications, through a copyright statement on the - * new code or document such as "This software or document includes material - * copied from or derived from [title and URI of the W3C document]. - * Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." - * - * Disclaimers - * - * THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS - * OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF - * MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE - * SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, - * TRADEMARKS OR OTHER RIGHTS. - * COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR - * CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. - * - * The name and trademarks of copyright holders may NOT be used in advertising or - * publicity pertaining to the work without specific, written prior permission. - * Title to copyright in this work will at all times remain with copyright holders. - */ -;/* - * Copyright 2015-2016 Canada Centre for Mapping and Earth Observation, - * Earth Sciences Sector, Natural Resources Canada. - * - * License - * - * By obtaining and/or copying this work, you (the licensee) agree that you have - * read, understood, and will comply with the following terms and conditions. - * - * Permission to copy, modify, and distribute this work, with or without - * modification, for any purpose and without fee or royalty is hereby granted, - * provided that you include the following on ALL copies of the work or portions - * thereof, including modifications: - * - * The full text of this NOTICE in a location viewable to users of the - * redistributed or derivative work. - * - * Any pre-existing intellectual property disclaimers, notices, or terms and - * conditions. If none exist, the W3C Software and Document Short Notice should - * be included. - * - * Notice of any changes or modifications, through a copyright statement on the - * new code or document such as "This software or document includes material - * copied from or derived from [title and URI of the W3C document]. - * Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." - * - * Disclaimers - * - * THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS - * OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF - * MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE - * SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, - * TRADEMARKS OR OTHER RIGHTS. - * COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR - * CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. - * - * The name and trademarks of copyright holders may NOT be used in advertising or - * publicity pertaining to the work without specific, written prior permission. - * Title to copyright in this work will at all times remain with copyright holders. - */ -/* global L, Node */ -/*jshint esversion: 6 */ -(function (window, document, undefined) { - -var M = {}; -window.M = M; - (function () { - M.detectImagePath = function (container) { - // this relies on the CSS style leaflet-default-icon-path containing a - // relative url() that leads to a valid icon file. Since that depends on - // how all of this stuff is deployed (i.e. custom element or as leaflet-plugin) - // also, because we're using 'shady DOM' api, the container must be - // a shady dom container, because the custom element tags it with added - // style-scope ... and related classes. - var el = L.DomUtil.create('div', 'leaflet-default-icon-path', container); - var path = L.DomUtil.getStyle(el, 'background-image') || - L.DomUtil.getStyle(el, 'backgroundImage'); // IE8 - - container.removeChild(el); - - if (path === null || path.indexOf('url') !== 0) { - path = ''; - } else { - path = path.replace(/^url\(["']?/, '').replace(/marker-icon\.png["']?\)$/, ''); - } - - return path; - }; - M.mime = "text/mapml"; - // see https://leafletjs.com/reference-1.5.0.html#crs-l-crs-base - // "new classes can't inherit from (L.CRS), and methods can't be added - // to (L.CRS.anything) with the include function - // so we'll use the options property as a way to integrate needed - // properties and methods... - M.WGS84 = new L.Proj.CRS('EPSG:4326','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ', { - origin: [-180,+90], - bounds: L.bounds([[-180,-90],[180,90]]), - resolutions: [ - 0.703125, - 0.3515625, - 0.17578125, - 0.087890625, - 0.0439453125, - 0.02197265625, - 0.010986328125, - 0.0054931640625, - 0.00274658203125, - 0.001373291015625, - 0.0006866455078125, - 0.0003433227539062, - 0.0001716613769531, - 0.0000858306884766, - 0.0000429153442383, - 0.0000214576721191, - 0.0000107288360596, - 0.0000053644180298, - 0.0000026822090149, - 0.0000013411045074, - 0.0000006705522537, - 0.0000003352761269 - ], - crs: { - tcrs: { - horizontal: { - name: "x", - min: 0, - max: zoom => (M.WGS84.options.bounds.getSize().x / M.WGS84.options.resolutions[zoom]).toFixed() - }, - vertical: { - name: "y", - min:0, - max: zoom => (M.WGS84.options.bounds.getSize().y / M.WGS84.options.resolutions[zoom]).toFixed() - }, - bounds: zoom => L.bounds([M.WGS84.options.crs.tcrs.horizontal.min, - M.WGS84.options.crs.tcrs.vertical.min], - [M.WGS84.options.crs.tcrs.horizontal.max(zoom), - M.WGS84.options.crs.tcrs.vertical.max(zoom)]) - }, - pcrs: { - horizontal: { - name: "longitude", - get min() {return M.WGS84.options.crs.gcrs.horizontal.min;}, - get max() {return M.WGS84.options.crs.gcrs.horizontal.max;} - }, - vertical: { - name: "latitude", - get min() {return M.WGS84.options.crs.gcrs.vertical.min;}, - get max() {return M.WGS84.options.crs.gcrs.vertical.max;} - }, - get bounds() {return M.WGS84.options.bounds;} - }, - gcrs: { - horizontal: { - name: "longitude", - // set min/max axis values from EPSG registry area of use, retrieved 2019-07-25 - min: -180.0, - max: 180.0 - }, - vertical: { - name: "latitude", - // set min/max axis values from EPSG registry area of use, retrieved 2019-07-25 - min: -90.0, - max: 90.0 - }, - get bounds() {return L.latLngBounds( - [M.WGS84.options.crs.gcrs.vertical.min,M.WGS84.options.crs.gcrs.horizontal.min], - [M.WGS84.options.crs.gcrs.vertical.max,M.WGS84.options.crs.gcrs.horizontal.max]);} - }, - map: { - horizontal: { - name: "i", - min: 0, - max: map => map.getSize().x - }, - vertical: { - name: "j", - min: 0, - max: map => map.getSize().y - }, - bounds: map => L.bounds(L.point([0,0]),map.getSize()) - }, - tile: { - horizontal: { - name: "i", - min: 0, - max: 256 - }, - vertical: { - name: "j", - min: 0, - max: 256 - }, - get bounds() {return L.bounds( - [M.WGS84.options.crs.tile.horizontal.min,M.WGS84.options.crs.tile.vertical.min], - [M.WGS84.options.crs.tile.horizontal.max,M.WGS84.options.crs.tile.vertical.max]);} - }, - tilematrix: { - horizontal: { - name: "column", - min: 0, - max: zoom => (M.WGS84.options.crs.tcrs.horizontal.max(zoom) / M.WGS84.options.crs.tile.bounds.getSize().x).toFixed() - }, - vertical: { - name: "row", - min: 0, - max: zoom => (M.WGS84.options.crs.tcrs.vertical.max(zoom) / M.WGS84.options.crs.tile.bounds.getSize().y).toFixed() - }, - bounds: zoom => L.bounds( - [M.WGS84.options.crs.tilematrix.horizontal.min, - M.WGS84.options.crs.tilematrix.vertical.min], - [M.WGS84.options.crs.tilematrix.horizontal.max(zoom), - M.WGS84.options.crs.tilematrix.vertical.max(zoom)]) - } - } - }); - M.CBMTILE = new L.Proj.CRS('EPSG:3978', - '+proj=lcc +lat_1=49 +lat_2=77 +lat_0=49 +lon_0=-95 +x_0=0 +y_0=0 +ellps=GRS80 +datum=NAD83 +units=m +no_defs', { - origin: [-34655800, 39310000], - bounds: L.bounds([[-34655800,-39000000],[10000000,39310000]]), - resolutions: [ - 38364.660062653464, - 22489.62831258996, - 13229.193125052918, - 7937.5158750317505, - 4630.2175937685215, - 2645.8386250105837, - 1587.5031750063501, - 926.0435187537042, - 529.1677250021168, - 317.50063500127004, - 185.20870375074085, - 111.12522225044451, - 66.1459656252646, - 38.36466006265346, - 22.48962831258996, - 13.229193125052918, - 7.9375158750317505, - 4.6302175937685215, - 2.6458386250105836, - 1.5875031750063502, - 0.92604351875370428, - 0.52916772500211673, - 0.31750063500127002, - 0.18520870375074083, - 0.11112522225044451, - 0.066145965625264591 - ], - crs: { - tcrs: { - horizontal: { - name: "x", - min: 0, - max: zoom => (M.CBMTILE.options.bounds.getSize().x / M.CBMTILE.options.resolutions[zoom]).toFixed() - }, - vertical: { - name: "y", - min:0, - max: zoom => (M.CBMTILE.options.bounds.getSize().y / M.CBMTILE.options.resolutions[zoom]).toFixed() - }, - bounds: zoom => L.bounds([M.CBMTILE.options.crs.tcrs.horizontal.min, - M.CBMTILE.options.crs.tcrs.vertical.min], - [M.CBMTILE.options.crs.tcrs.horizontal.max(zoom), - M.CBMTILE.options.crs.tcrs.vertical.max(zoom)]) - }, - pcrs: { - horizontal: { - name: "easting", - get min() {return M.CBMTILE.options.bounds.min.x;}, - get max() {return M.CBMTILE.options.bounds.max.x;} - }, - vertical: { - name: "northing", - get min() {return M.CBMTILE.options.bounds.min.y;}, - get max() {return M.CBMTILE.options.bounds.max.y;} - }, - get bounds() {return M.CBMTILE.options.bounds;} - }, - gcrs: { - horizontal: { - name: "longitude", - // set min/max axis values from EPSG registry area of use, retrieved 2019-07-25 - min: -141.01, - max: -47.74 - }, - vertical: { - name: "latitude", - // set min/max axis values from EPSG registry area of use, retrieved 2019-07-25 - min: 40.04, - max: 86.46 - }, - get bounds() {return L.latLngBounds( - [M.CBMTILE.options.crs.gcrs.vertical.min,M.CBMTILE.options.crs.gcrs.horizontal.min], - [M.CBMTILE.options.crs.gcrs.vertical.max,M.CBMTILE.options.crs.gcrs.horizontal.max]);} - }, - map: { - horizontal: { - name: "i", - min: 0, - max: map => map.getSize().x - }, - vertical: { - name: "j", - min: 0, - max: map => map.getSize().y - }, - bounds: map => L.bounds(L.point([0,0]),map.getSize()) - }, - tile: { - horizontal: { - name: "i", - min: 0, - max: 256 - }, - vertical: { - name: "j", - min: 0, - max: 256 - }, - get bounds() {return L.bounds( - [M.CBMTILE.options.crs.tile.horizontal.min,M.CBMTILE.options.crs.tile.vertical.min], - [M.CBMTILE.options.crs.tile.horizontal.max,M.CBMTILE.options.crs.tile.vertical.max]);} - }, - tilematrix: { - horizontal: { - name: "column", - min: 0, - max: zoom => (M.CBMTILE.options.crs.tcrs.horizontal.max(zoom) / M.CBMTILE.options.crs.tile.bounds.getSize().x).toFixed() - }, - vertical: { - name: "row", - min: 0, - max: zoom => (M.CBMTILE.options.crs.tcrs.vertical.max(zoom) / M.CBMTILE.options.crs.tile.bounds.getSize().y).toFixed() - }, - bounds: zoom => L.bounds([0,0], - [M.CBMTILE.options.crs.tilematrix.horizontal.max(zoom), - M.CBMTILE.options.crs.tilematrix.vertical.max(zoom)]) - } - } - }); - M.APSTILE = new L.Proj.CRS('EPSG:5936', - '+proj=stere +lat_0=90 +lat_ts=50 +lon_0=-150 +k=0.994 +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs', { - origin: [-2.8567784109255E7, 3.2567784109255E7], - bounds: L.bounds([[-28567784.109254867,-28567784.109254755],[32567784.109255023,32567784.10925506]]), - resolutions: [ - 238810.813354, - 119405.406677, - 59702.7033384999, - 29851.3516692501, - 14925.675834625, - 7462.83791731252, - 3731.41895865639, - 1865.70947932806, - 932.854739664032, - 466.427369832148, - 233.213684916074, - 116.606842458037, - 58.3034212288862, - 29.1517106145754, - 14.5758553072877, - 7.28792765351156, - 3.64396382688807, - 1.82198191331174, - 0.910990956788164, - 0.45549547826179 - ], - crs: { - tcrs: { - horizontal: { - name: "x", - min: 0, - max: zoom => (M.APSTILE.options.bounds.getSize().x / M.APSTILE.options.resolutions[zoom]).toFixed() - }, - vertical: { - name: "y", - min:0, - max: zoom => (M.APSTILE.options.bounds.getSize().y / M.APSTILE.options.resolutions[zoom]).toFixed() - }, - bounds: zoom => L.bounds([M.APSTILE.options.crs.tcrs.horizontal.min, - M.APSTILE.options.crs.tcrs.vertical.min], - [M.APSTILE.options.crs.tcrs.horizontal.max(zoom), - M.APSTILE.options.crs.tcrs.vertical.max(zoom)]) - }, - pcrs: { - horizontal: { - name: "easting", - get min() {return M.APSTILE.options.bounds.min.x;}, - get max() {return M.APSTILE.options.bounds.max.x;} - }, - vertical: { - name: "northing", - get min() {return M.APSTILE.options.bounds.min.y;}, - get max() {return M.APSTILE.options.bounds.max.y;} - }, - get bounds() {return M.APSTILE.options.bounds;} - }, - gcrs: { - horizontal: { - name: "longitude", - // set min/max axis values from EPSG registry area of use, retrieved 2019-07-25 - min: -180.0, - max: 180.0 - }, - vertical: { - name: "latitude", - // set min/max axis values from EPSG registry area of use, retrieved 2019-07-25 - min: 60.0, - max: 90.0 - }, - get bounds() {return L.latLngBounds( - [M.APSTILE.options.crs.gcrs.vertical.min,M.APSTILE.options.crs.gcrs.horizontal.min], - [M.APSTILE.options.crs.gcrs.vertical.max,M.APSTILE.options.crs.gcrs.horizontal.max]);} - }, - map: { - horizontal: { - name: "i", - min: 0, - max: map => map.getSize().x - }, - vertical: { - name: "j", - min: 0, - max: map => map.getSize().y - }, - bounds: map => L.bounds(L.point([0,0]),map.getSize()) - }, - tile: { - horizontal: { - name: "i", - min: 0, - max: 256 - }, - vertical: { - name: "j", - min: 0, - max: 256 - }, - get bounds() {return L.bounds( - [M.APSTILE.options.crs.tile.horizontal.min,M.APSTILE.options.crs.tile.vertical.min], - [M.APSTILE.options.crs.tile.horizontal.max,M.APSTILE.options.crs.tile.vertical.max]);} - }, - tilematrix: { - horizontal: { - name: "column", - min: 0, - max: zoom => (M.APSTILE.options.crs.tcrs.horizontal.max(zoom) / M.APSTILE.options.crs.tile.bounds.getSize().x).toFixed() - }, - vertical: { - name: "row", - min: 0, - max: zoom => (M.APSTILE.options.crs.tcrs.vertical.max(zoom) / M.APSTILE.options.crs.tile.bounds.getSize().y).toFixed() - }, - bounds: zoom => L.bounds([0,0], - [M.APSTILE.options.crs.tilematrix.horizontal.max(zoom), - M.APSTILE.options.crs.tilematrix.vertical.max(zoom)]) - } - } - }); - M.OSMTILE = L.CRS.EPSG3857; - L.setOptions(M.OSMTILE, { - origin: [-20037508.342787, 20037508.342787], - bounds: L.bounds([[-20037508.342787, -20037508.342787],[20037508.342787, 20037508.342787]]), - resolutions: [ - 156543.0339, - 78271.51695, - 39135.758475, - 19567.8792375, - 9783.93961875, - 4891.969809375, - 2445.9849046875, - 1222.9924523438, - 611.49622617188, - 305.74811308594, - 152.87405654297, - 76.437028271484, - 38.218514135742, - 19.109257067871, - 9.5546285339355, - 4.7773142669678, - 2.3886571334839, - 1.1943285667419, - 0.59716428337097, - 0.29858214168549, - 0.14929107084274, - 0.074645535421371, - 0.03732276771068573, - 0.018661383855342865, - 0.009330691927671432495 - ], - crs: { - tcrs: { - horizontal: { - name: "x", - min: 0, - max: zoom => (M.OSMTILE.options.bounds.getSize().x / M.OSMTILE.options.resolutions[zoom]).toFixed() - }, - vertical: { - name: "y", - min:0, - max: zoom => (M.OSMTILE.options.bounds.getSize().y / M.OSMTILE.options.resolutions[zoom]).toFixed() - }, - bounds: zoom => L.bounds([M.OSMTILE.options.crs.tcrs.horizontal.min, - M.OSMTILE.options.crs.tcrs.vertical.min], - [M.OSMTILE.options.crs.tcrs.horizontal.max(zoom), - M.OSMTILE.options.crs.tcrs.vertical.max(zoom)]) - }, - pcrs: { - horizontal: { - name: "easting", - get min() {return M.OSMTILE.options.bounds.min.x;}, - get max() {return M.OSMTILE.options.bounds.max.x;} - }, - vertical: { - name: "northing", - get min() {return M.OSMTILE.options.bounds.min.y;}, - get max() {return M.OSMTILE.options.bounds.max.y;} - }, - get bounds() {return M.OSMTILE.options.bounds;} - }, - gcrs: { - horizontal: { - name: "longitude", - get min() {return M.OSMTILE.unproject(M.OSMTILE.options.bounds.min).lng;}, - get max() {return M.OSMTILE.unproject(M.OSMTILE.options.bounds.max).lng;} - }, - vertical: { - name: "latitude", - get min() {return M.OSMTILE.unproject(M.OSMTILE.options.bounds.min).lat;}, - get max() {return M.OSMTILE.unproject(M.OSMTILE.options.bounds.max).lat;} - }, - get bounds() {return L.latLngBounds( - [M.OSMTILE.options.crs.gcrs.vertical.min,M.OSMTILE.options.crs.gcrs.horizontal.min], - [M.OSMTILE.options.crs.gcrs.vertical.max,M.OSMTILE.options.crs.gcrs.horizontal.max]);} - }, - map: { - horizontal: { - name: "i", - min: 0, - max: map => map.getSize().x - }, - vertical: { - name: "j", - min: 0, - max: map => map.getSize().y - }, - bounds: map => L.bounds(L.point([0,0]),map.getSize()) - }, - tile: { - horizontal: { - name: "i", - min: 0, - max: 256 - }, - vertical: { - name: "j", - min: 0, - max: 256 - }, - get bounds() {return L.bounds( - [M.OSMTILE.options.crs.tile.horizontal.min,M.OSMTILE.options.crs.tile.vertical.min], - [M.OSMTILE.options.crs.tile.horizontal.max,M.OSMTILE.options.crs.tile.vertical.max]);} - }, - tilematrix: { - horizontal: { - name: "column", - min: 0, - max: zoom => (M.OSMTILE.options.crs.tcrs.horizontal.max(zoom) / M.OSMTILE.options.crs.tile.bounds.getSize().x).toFixed() - }, - vertical: { - name: "row", - min: 0, - max: zoom => (M.OSMTILE.options.crs.tcrs.vertical.max(zoom) / M.OSMTILE.options.crs.tile.bounds.getSize().y).toFixed() - }, - bounds: zoom => L.bounds([0,0], - [M.OSMTILE.options.crs.tilematrix.horizontal.max(zoom), - M.OSMTILE.options.crs.tilematrix.vertical.max(zoom)]) - } - } - }); -}()); -M.Util = { - coordsToArray: function(containerPoints) { - // returns an array of arrays of coordinate pairs coordsToArray("1,2,3,4") -> [[1,2],[3,4]] - for (var i=1, pairs = [], coords = containerPoints.split(",");i element, so we can - // use its layers property to iterate the layers from top down - // evaluating if they are 'on the map' (enabled) - L.setOptions(this, {mapEl: this._map.options.mapEl}); - L.DomEvent.on(this._map, 'click', this._queryTopLayer, this); - L.DomEvent.on(this._map, 'keypress', this._queryTopLayerAtMapCenter, this); + 'use strict'; + + const TILE_SIZE = 256; + const FALLBACK_PROJECTION = "OSMTILE"; + const FALLBACK_CS = "TILEMATRIX"; + + var MapMLStaticTileLayer = L.GridLayer.extend({ + + initialize: function (options) { + this.zoomBounds = this._getZoomBounds(options.tileContainer,options.maxZoomBound); + L.extend(options, this.zoomBounds); + L.setOptions(this, options); + this._groups = this._groupTiles(this.options.tileContainer.getElementsByTagName('tile')); }, - removeHooks: function() { - L.DomEvent.off(this._map, 'click', this._queryTopLayer, this); - L.DomEvent.on(this._map, 'keypress', this._queryTopLayerAtMapCenter, this); + + onAdd: function(){ + this._bounds = this._getLayerBounds(this._groups,this._map.options.projection); //stores meter values of bounds + this.layerBounds = this._bounds[Object.keys(this._bounds)[0]]; + for(let key of Object.keys(this._bounds)){ + this.layerBounds.extend(this._bounds[key].min); + this.layerBounds.extend(this._bounds[key].max); + } + L.GridLayer.prototype.onAdd.call(this,this._map); + this._map.fire('moveend',true); }, - _getTopQueryableLayer: function() { - var layers = this.options.mapEl.layers; - // work backwards in document order (top down) - for (var l=layers.length-1;l>=0;l--) { - var mapmlLayer = layers[l]._layer; - if (layers[l].checked && mapmlLayer.queryable) { - return mapmlLayer; - } - } + + getEvents: function(){ + let events = L.GridLayer.prototype.getEvents.call(this,this._map); + this._parentOnMoveEnd = events.moveend; + events.moveend = this._handleMoveEnd; + events.move = ()=>{}; //needed to prevent moveend from running + return events; }, - _queryTopLayerAtMapCenter: function (event) { - if (event.originalEvent.key === " ") { - this._map.fire('click', { - latlng: this._map.getCenter(), - layerPoint: this._map.latLngToLayerPoint(this._map.getCenter()), - containerPoint: this._map.latLngToContainerPoint(this._map.getCenter()) - }); - } + + + //sets the bounds flag of the layer and calls default moveEnd if within bounds + //its the zoom level is between the nativeZoom and zoom then it uses the nativeZoom value to get the bound its checking + _handleMoveEnd : function(e){ + let mapZoom = this._map.getZoom(); + let zoomLevel = mapZoom; + zoomLevel = zoomLevel > this.options.maxNativeZoom? this.options.maxNativeZoom: zoomLevel; + zoomLevel = zoomLevel < this.options.minNativeZoom? this.options.minNativeZoom: zoomLevel; + this.isVisible = mapZoom <= this.zoomBounds.maxZoom && mapZoom >= this.zoomBounds.minZoom && + this._bounds[zoomLevel] && this._bounds[zoomLevel] + .overlaps(M.pixelToPCRSBounds( + this._map.getPixelBounds(), + this._map.getZoom(), + this._map.options.projection)); + + if(!(this.isVisible))return; //onMoveEnd still gets fired even when layer is out of bounds??, most likely need to overrride _onMoveEnd + this._parentOnMoveEnd(); }, - _queryTopLayer: function(event) { - var layer = this._getTopQueryableLayer(); - if (layer) { - this._query(event, layer); - } + + _isValidTile(coords) { + return this._groups[this._tileCoordsToKey(coords)]; }, - _query(e, layer) { - var obj = {}, - template = layer.getQueryTemplates()[0], - zoom = e.target.getZoom(), - map = this._map, - crs = layer.crs, - container = layer._container, - popupOptions = {autoPan: true, maxHeight: (map.getSize().y * 0.5) - 50}, - tcrs2pcrs = function (c) { - return crs.transformation.untransform(c,crs.scale(zoom)); - }, - tcrs2gcrs = function (c) { - return crs.unproject(crs.transformation.untransform(c,crs.scale(zoom)),zoom); - }; - var tcrsClickLoc = crs.latLngToPoint(e.latlng, zoom), - tileMatrixClickLoc = tcrsClickLoc.divideBy(256).floor(), - tileBounds = new L.Bounds(tcrsClickLoc.divideBy(256).floor().multiplyBy(256), tcrsClickLoc.divideBy(256).ceil().multiplyBy(256)); - - // all of the following are locations that might be used in a query, I think. - obj[template.query.tilei] = tcrsClickLoc.x.toFixed() - (tileMatrixClickLoc.x * 256); - obj[template.query.tilej] = tcrsClickLoc.y.toFixed() - (tileMatrixClickLoc.y * 256); - - // this forces the click to the centre of the map extent in the layer crs - obj[template.query.mapi] = (map.getSize().divideBy(2)).x.toFixed(); - obj[template.query.mapj] = (map.getSize().divideBy(2)).y.toFixed(); - - obj[template.query.pixelleft] = crs.pointToLatLng(tcrsClickLoc, zoom).lng; - obj[template.query.pixeltop] = crs.pointToLatLng(tcrsClickLoc, zoom).lat; - obj[template.query.pixelright] = crs.pointToLatLng(tcrsClickLoc.add([1,1]), zoom).lng; - obj[template.query.pixelbottom] = crs.pointToLatLng(tcrsClickLoc.add([1,1]), zoom).lat; - - obj[template.query.column] = tileMatrixClickLoc.x; - obj[template.query.row] = tileMatrixClickLoc.y; - obj[template.query.x] = tcrsClickLoc.x.toFixed(); - obj[template.query.y] = tcrsClickLoc.y.toFixed(); - - // whereas the layerPoint is calculated relative to the origin plus / minus any - // pan movements so is equal to containerPoint at first before any pans, but - // changes as the map pans. - obj[template.query.easting] = tcrs2pcrs(tcrsClickLoc).x; - obj[template.query.northing] = tcrs2pcrs(tcrsClickLoc).y; - obj[template.query.longitude] = tcrs2gcrs(tcrsClickLoc).lng; - obj[template.query.latitude] = tcrs2gcrs(tcrsClickLoc).lat; - obj[template.query.zoom] = zoom; - obj[template.query.width] = map.getSize().x; - obj[template.query.height] = map.getSize().y; - // assumes the click is at the centre of the map, per template.query.mapi, mapj above - obj[template.query.mapbottom] = tcrs2pcrs(tcrsClickLoc.add(map.getSize().divideBy(2))).y; - obj[template.query.mapleft] = tcrs2pcrs(tcrsClickLoc.subtract(map.getSize().divideBy(2))).x; - obj[template.query.maptop] = tcrs2pcrs(tcrsClickLoc.subtract(map.getSize().divideBy(2))).y; - obj[template.query.mapright] = tcrs2pcrs(tcrsClickLoc.add(map.getSize().divideBy(2))).x; + + createTile: function (coords) { + let tileGroup = this._groups[this._tileCoordsToKey(coords)] || [], + tileElem = document.createElement('tile'); + tileElem.setAttribute("col",coords.x); + tileElem.setAttribute("row",coords.y); + tileElem.setAttribute("zoom",coords.z); - obj[template.query.tilebottom] = tcrs2pcrs(tileBounds.max).y; - obj[template.query.tileleft] = tcrs2pcrs(tileBounds.min).x; - obj[template.query.tiletop] = tcrs2pcrs(tileBounds.min).y; - obj[template.query.tileright] = tcrs2pcrs(tileBounds.max).x; - // add hidden or other variables that may be present into the values to - // be processed by L.Util.template below. - for (var v in template.query) { - if (["mapi","mapj","tilei","tilej","row","col","x","y","easting","northing","longitude","latitude","width","height","zoom","mapleft","mapright",",maptop","mapbottom","tileleft","tileright","tiletop","tilebottom","pixeltop","pixelbottom","pixelleft","pixelright"].indexOf(v) < 0) { - obj[v] = template.query[v]; - } - } - fetch(L.Util.template(template.template, obj),{redirect: 'follow'}).then( - function(response) { - if (response.status >= 200 && response.status < 300) { - return Promise.resolve(response); - } else { - console.log('Looks like there was a problem. Status Code: ' + response.status); - return Promise.reject(response); - } - }).then(function(response) { - var contenttype = response.headers.get("Content-Type"); - if ( contenttype.startsWith("text/mapml")) { - return handleMapMLResponse(response, e.latlng); - } else { - return handleOtherResponse(response, layer, e.latlng); - } - }).catch(function(err) { - // no op - }); - function handleMapMLResponse(response, loc) { - return response.text().then(mapml => { - // bind the deprojection function of the layer's crs - var _unproject = L.bind(crs.unproject, crs); - function _coordsToLatLng(coords) { - return _unproject(L.point(coords)); - } - var parser = new DOMParser(), - mapmldoc = parser.parseFromString(mapml, "application/xml"), - f = M.mapMlFeatures(mapmldoc, { - // pass the vector layer a renderer of its own, otherwise leaflet - // puts everything into the overlayPane - renderer: L.svg(), - // pass the vector layer the container for the parent into which - // it will append its own container for rendering into - pane: container, - color: 'yellow', - // instead of unprojecting and then projecting and scaling, - // a much smarter approach would be to scale at the current - // zoom - coordsToLatLng: _coordsToLatLng, - imagePath: M.detectImagePath(map.getContainer()) - }); - f.addTo(map); - - var c = document.createElement('iframe'); - c.csp = "script-src 'none'"; - c.style = "border: none"; - c.srcdoc = mapmldoc.querySelector('feature properties').innerHTML; - - // passing a latlng to the popup is necessary for when there is no - // geometry / null geometry - layer.bindPopup(c, popupOptions).openPopup(loc); - layer.on('popupclose', function() { - map.removeLayer(f); - }); - }); + for(let i = 0;i { - var c = document.createElement('iframe'); - c.csp = "script-src 'none'"; - c.style = "border: none"; - c.srcdoc = text; - layer.bindPopup(c, popupOptions).openPopup(loc); - }); - } - } -}); -// see https://leafletjs.com/examples/extending/extending-3-controls.html#handlers -L.Map.addInitHook('addHandler', 'query', M.QueryHandler); -M.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) - // e.g. in the raw MapML-Leaflet-Client index.html page. - options: { - maxNext: 10, - zIndex: 0, - maxZoom: 25 - }, - // initialize is executed before the layer is added to a map - initialize: function (href, content, options) { - // in the custom element, the attribute is actually 'src' - // the _href version is the URL received from layer-@src - if (href) { - this._href = href; - } - this._layerEl = content; - var mapml = content.querySelector('image,feature,tile,extent') ? true : false; - if (mapml) { - this._content = content; - } - this._container = L.DomUtil.create('div', 'leaflet-layer'); - L.DomUtil.addClass(this._container,'mapml-layer'); - this._imageContainer = L.DomUtil.create('div', 'leaflet-layer', this._container); - L.DomUtil.addClass(this._imageContainer,'mapml-image-container'); - - // this layer 'owns' a mapmlTileLayer, which is a subclass of L.GridLayer - // it 'passes' what tiles to load via the content of this._mapmlTileContainer - this._mapmlTileContainer = L.DomUtil.create('div', 'mapml-tile-container', this._container); - // hit the service to determine what its extent might be - // OR use the extent of the content provided - this._initCount = 0; - this._initExtent(mapml ? content : null); - - // a default extent can't be correctly set without the map to provide - // its bounds , projection, zoom range etc, so if that stuff's not - // 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. - // may revisit some time. - L.setOptions(this, options); + return tileElem; }, - setZIndex: function (zIndex) { - this.options.zIndex = zIndex; - this._updateZIndex(); - return this; - }, - _updateZIndex: function () { - if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) { - this._container.style.zIndex = this.options.zIndex; + _getLayerBounds: function(tileGroups, projection){ + let layerBounds = {}; + for(let tile in tileGroups){ + let sCoords = tile.split(":"), pixelCoords = {}; + pixelCoords.x = +sCoords[0] * TILE_SIZE; + pixelCoords.y = +sCoords[1] * TILE_SIZE; + pixelCoords.z = +sCoords[2]; //+String same as parseInt(String) + if(sCoords[2] in layerBounds){ + + layerBounds[sCoords[2]].extend(L.point(pixelCoords.x ,pixelCoords.y )); + layerBounds[sCoords[2]].extend(L.point(((pixelCoords.x+TILE_SIZE) ),((pixelCoords.y+TILE_SIZE) ))); + } else { + layerBounds[sCoords[2]] = L.bounds( + L.point(pixelCoords.x ,pixelCoords.y ), + L.point(((pixelCoords.x+TILE_SIZE) ),((pixelCoords.y+TILE_SIZE) ))); } - }, - _changeOpacity: function(e) { - if (e && e.target && e.target.value >=0 && e.target.value <= 1.0) { - this.changeOpacity(e.target.value); } + // TODO: optimize by removing 2nd loop, add util function to convert point in pixels to point in pcrs, use that instead then this loop + // won't be needed + for(let pixelBounds in layerBounds){ + let zoom = +pixelBounds; + layerBounds[pixelBounds] = M.pixelToPCRSBounds(layerBounds[pixelBounds],zoom,projection); + } + + return layerBounds; }, - changeOpacity: function(opacity) { - this._container.style.opacity = opacity; + + _getZoomBounds: function(container, maxZoomBound){ + if(!container) return null; + let meta = M.metaContentToObject(container.getElementsByTagName('tiles')[0].getAttribute('zoom')), + zoom = {},tiles = container.getElementsByTagName("tile"); + zoom.maxNativeZoom = 0; + zoom.minNativeZoom = maxZoomBound; + for (let i=0;i zoom.maxNativeZoom) zoom.maxNativeZoom = +tiles[i].getAttribute('zoom'); + if(+tiles[i].getAttribute('zoom') < zoom.minNativeZoom) zoom.minNativeZoom = +tiles[i].getAttribute('zoom'); + } + + //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; + return zoom; }, - onAdd: function (map) { - this._map = map; - if (!this._mapmlvectors) { - this._mapmlvectors = M.mapMlFeatures(this._content, { - // pass the vector layer a renderer of its own, otherwise leaflet - // puts everything into the overlayPane - renderer: L.svg(), - // pass the vector layer the container for the parent into which - // it will append its own container for rendering into - pane: this._container, - opacity: this.options.opacity, - imagePath: M.detectImagePath(this._map.getContainer()), - // each owned child layer gets a reference to the root layer - _leafletLayer: this, - onEachFeature: function(properties, geometry) { - // need to parse as HTML to preserve semantics and styles - if (properties) { - var c = document.createElement('div'); - c.insertAdjacentHTML('afterbegin', properties.innerHTML); - geometry.bindPopup(c, {autoPan:false}); - } - } - }); - } - map.addLayer(this._mapmlvectors); - - if (!this._imageLayer) { - this._imageLayer = L.layerGroup(); - } - map.addLayer(this._imageLayer); - // the layer._imageContainer property contains an element in which - // content will be maintained - - if (!this._tileLayer) { - this._tileLayer = M.mapMLTileLayer(this.href?this.href:this._href, - {pane: this._container, - _leafletLayer: this}); - } - this._tileLayer._mapmlTileContainer = this._mapmlTileContainer; - map.addLayer(this._tileLayer); - this._tileLayer._container.appendChild(this._mapmlTileContainer); - // if the extent has been initialized and received, update the map, - if (this._extent) { - if (this._templateVars) { - this._templatedLayer = M.templatedLayer(this._templateVars, - { pane: this._container, - imagePath: M.detectImagePath(this._map.getContainer()), - _leafletLayer: this, - crs: this.crs - }).addTo(map); - } + + _groupTiles: function (tiles) { + let tileMap = {}; + for (let i=0;i get content when - // that is available. - this.once('extentload', this._onMoveEnd, this); - } - this.setZIndex(this.options.zIndex); - this.getPane().appendChild(this._container); - }, - addTo: function (map) { - map.addLayer(this); - return this; - }, - getEvents: function () { - return { - moveend: this._onMoveEnd, - zoomanim: this._onZoomAnim - }; - }, - redraw: function() { - // for now, only redraw templated layers. - if (this._templatedLayer) { - this._templatedLayer.redraw(); - } - }, - _onZoomAnim: function(e) { - var toZoom = e.zoom, - zoom = this._extent.querySelector("input[type=zoom]"), - min = zoom && zoom.hasAttribute("min") ? parseInt(zoom.getAttribute("min")) : this._map.getMinZoom(), - max = zoom && zoom.hasAttribute("max") ? parseInt(zoom.getAttribute("max")) : this._map.getMaxZoom(), - canZoom = (toZoom < min && this._extent.zoomout) || (toZoom > max && this._extent.zoomin); - if (!this._extent.hasAttribute('action') && !(min <= toZoom && toZoom <= max)){ - if (this._extent.zoomin && toZoom > max) { - // this._href is the 'original' url from which this layer came - // since we are following a zoom link we will be getting a new - // layer almost, resetting child content as appropriate - this._href = this._extent.zoomin; - // this.href is the "public" property. When a dynamic layer is - // accessed, this value changes with every new extent received - this.href = this._extent.zoomin; - } else if (this._extent.zoomout && toZoom < min) { - this._href = this._extent.zoomout; - this.href = this._extent.zoomout; + tileMap[tileCode]=[tile]; } } - if (this._templatedLayer && canZoom ) { - // get the new extent - this._initExtent(); - } + return tileMap; }, - onRemove: function (map) { - L.DomUtil.remove(this._container); - map.removeLayer(this._mapmlvectors); - map.removeLayer(this._tileLayer); - map.removeLayer(this._imageLayer); - if (this._templatedLayer) { - map.removeLayer(this._templatedLayer); + }); + + var mapMLStaticTileLayer = function(options) { + return new MapMLStaticTileLayer(options); + }; + + var MapMLLayerControl = L.Control.Layers.extend({ + /* removes 'base' layers as a concept */ + options: { + autoZIndex: false, + sortLayers: true, + sortFunction: function (layerA, layerB) { + return layerA.options.zIndex < layerB.options.zIndex ? -1 : (layerA.options.zIndex > layerB.options.zIndex ? 1 : 0); } - }, - getZoomBounds: function () { - var ext = this._extent; - var zoom = ext ? ext.querySelector('[type=zoom]') : undefined, - min = zoom && zoom.hasAttribute('min') ? zoom.getAttribute('min') : this._map.getMinZoom(), - max = zoom && zoom.hasAttribute('max') ? zoom.getAttribute('max') : this._map.getMaxZoom(); - var bounds = {}; - bounds.min = Math.min(min,max); - bounds.max = Math.max(min,max); - return bounds; - }, - _transformDeprectatedInput: function (i) { - var type = i.getAttribute("type").toLowerCase(); - if (type === "xmin" || type === "ymin" || type === "xmax" || type === "ymax") { - i.setAttribute("type", "location"); - i.setAttribute("units","tcrs"); - switch (type) { - case "xmin": - i.setAttribute("axis","x"); - i.setAttribute("position","top-left"); - break; - case "ymin": - i.setAttribute("axis","y"); - i.setAttribute("position","top-left"); - break; - case "xmax": - i.setAttribute("axis","x"); - i.setAttribute("position","bottom-right"); - break; - case "ymax": - i.setAttribute("axis","y"); - i.setAttribute("position","bottom-right"); + }, + initialize: function (overlays, options) { + L.setOptions(this, options); + + // the _layers array contains objects like {layer: layer, name: "name", overlay: true} + // the array index is the id of the layer returned by L.stamp(layer) which I guess is a unique hash + this._layerControlInputs = []; + this._layers = []; + this._lastZIndex = 0; + this._handlingClick = false; + + for (var i in overlays) { + this._addLayer(overlays[i], i, true); + } + }, + onAdd: function () { + this._initLayout(); + this._map.on('moveend', this._validateExtents, this); + this._update(); + //this._validateExtents(); + return this._container; + }, + onRemove: function (map) { + map.off('moveend', this._validateExtents, this); + // 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); + this._layers[i].layer.off('extentload', this._validateExtents, this); + } + }, + addOrUpdateOverlay: function (layer, name) { + var alreadyThere = false; + for (var i=0;i - switch (axis) { - case ('easting'): - if (position) { - if (position.match(/.*?-left/i)) { - extentVarNames.extent.left = { name: name, axis: axis}; - } else if (position.match(/.*?-right/i)) { - extentVarNames.extent.right = { name: name, axis: axis}; - } - } - break; - case ('northing'): - if (position) { - if (position.match(/top-.*?/i)) { - extentVarNames.extent.top = { name: name, axis: axis}; - } else if (position.match(/bottom-.*?/i)) { - extentVarNames.extent.bottom = { name: name, axis: axis}; - } - } - break; - case ('x'): - if (position) { - if (position.match(/.*?-left/i)) { - extentVarNames.extent.left = { name: name, axis: axis}; - } else if (position.match(/.*?-right/i)) { - extentVarNames.extent.right = { name: name, axis: axis}; - } - } - break; - case ('y'): - if (position) { - if (position.match(/top-.*?/i)) { - extentVarNames.extent.top = { name: name, axis: axis}; - } else if (position.match(/bottom-.*?/i)) { - extentVarNames.extent.bottom = { name: name, axis: axis}; - } - } - break; - case ('longitude'): - if (position) { - if (position.match(/.*?-left/i)) { - extentVarNames.extent.left = { name: name, axis: axis}; - } else if (position.match(/.*?-right/i)) { - extentVarNames.extent.right = { name: name, axis: axis}; - } - } - break; - case ('latitude'): - if (position) { - if (position.match(/top-.*?/i)) { - extentVarNames.extent.top = { name: name, axis: axis}; - } else if (position.match(/bottom-.*?/i)) { - extentVarNames.extent.bottom = { name: name, axis: axis}; - } - } - break; } - // projection is deprecated, make it hidden - } else if (type === "hidden" || type === "projection") { - extentVarNames.extent.hidden.push({name: name, value: value}); - } - } - return extentVarNames; - }, - // retrieve the (projected, scaled) layer extent for the current map zoom level - getLayerExtentBounds: function(map) { - - if (!this._extent) return; - var zoom = map.getZoom(), projection = map.options.projection, - ep = this._extent.getAttribute("units"), - projecting = (projection !== ep), - p; - - var xmin,ymin,xmax,ymax,v1,v2,extentZoomValue; - - // todo: create an array of min values, converted to tcrs units - // take the Math.min of all of them. - v1 = this._extent.querySelector('[type=xmin]').getAttribute('min'); - v2 = this._extent.querySelector('[type=xmax]').getAttribute('min'); - xmin = Math.min(v1,v2); - v1 = this._extent.querySelector('[type=xmin]').getAttribute('max'); - v2 = this._extent.querySelector('[type=xmax]').getAttribute('max'); - xmax = Math.max(v1,v2); - v1 = this._extent.querySelector('[type=ymin]').getAttribute('min'); - v2 = this._extent.querySelector('[type=ymax]').getAttribute('min'); - ymin = Math.min(v1,v2); - v1 = this._extent.querySelector('[type=ymin]').getAttribute('max'); - v2 = this._extent.querySelector('[type=ymax]').getAttribute('max'); - ymax = Math.max(v1,v2); - // WGS84 can be converted to Tiled CRS units - if (projecting) { - //project and scale to M[projection] from WGS84 - p = M[projection]; - var corners = [ - p.latLngToPoint(L.latLng([ymin,xmin]),zoom), - p.latLngToPoint(L.latLng([ymax,xmax]),zoom), - p.latLngToPoint(L.latLng([ymin,xmin]),zoom), - p.latLngToPoint(L.latLng([ymin,xmax]),zoom) - ]; - return L.bounds(corners); - } else { - // if the zoom level of the extent does not match that of the map - extentZoomValue = parseInt(this._extent.querySelector('[type=zoom]').getAttribute('value')); - if (extentZoomValue !== zoom) { - // convert the extent bounds to corresponding bounds at the current map zoom - p = M[projection]; - return L.bounds( - p.latLngToPoint(p.pointToLatLng(L.point(xmin,ymin),extentZoomValue),zoom), - p.latLngToPoint(p.pointToLatLng(L.point(xmax,ymax),extentZoomValue),zoom)); - } else { - // the extent's zoom value === map.getZoom(), return the bounds - return L.bounds(L.point(xmin,ymin), L.point(xmax,ymax)); - } } - }, - getAttribution: function () { - return this.options.attribution; - }, - getLayerUserControlsHTML: function () { - var label = document.createElement('label'), - input = document.createElement('input'), - name = document.createElement('span'), - details = document.createElement('details'), - summary = document.createElement('summary'), - opacity = document.createElement('input'), - opacityControl = document.createElement('details'), - opacityControlSummary = document.createElement('summary'); - - input.defaultChecked = this._map ? true: false; - input.type = 'checkbox'; - input.className = 'leaflet-control-layers-selector'; - name.draggable = true; - - if (this._legendUrl) { - var legendLink = document.createElement('a'); - legendLink.text = ' ' + this._title; - legendLink.href = this._legendUrl; - legendLink.target = '_blank'; - name.appendChild(legendLink); - } else { - name.innerHTML = ' ' + this._title; + if (!alreadyThere) { + this.addOverlay(layer, name); } - - opacityControlSummary.innerText = 'opacity'; - opacityControl.appendChild(opacityControlSummary); - opacityControl.appendChild(opacity); - L.DomUtil.addClass(details, 'mapml-control-layers'); - L.DomUtil.addClass(opacityControl,'mapml-control-layers'); - opacity.setAttribute('type','range'); - opacity.setAttribute('min', '0'); - opacity.setAttribute('max','1.0'); - opacity.setAttribute('value', '1.0'); - opacity.setAttribute('step','0.1'); - - L.DomEvent.on(opacity,'change', this._changeOpacity, this); - L.DomEvent.on(name,'dragstart', function(event) { - // will have to figure out how to drag and drop a whole element - // with its contents in the case where the content - // has no src but does have inline content. - // Should be do-able, I think. - if (this._href) { - event.dataTransfer.setData("text/uri-list",this._href); - // Why use a second .setData("text/plain"...) ? This is very important: - // See https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#link - event.dataTransfer.setData("text/plain", this._href); - } - }, this); - - details.appendChild(summary); - label.appendChild(details); - summary.appendChild(input); - summary.appendChild(name); - details.appendChild(opacityControl); - - if (this._styles) { - details.appendChild(this._styles); - } - if (this._userInputs) { - var frag = document.createDocumentFragment(); - var templates = this._templateVars; - if (templates) { - for (var i=0;i once - if (mapmlInput.tagName.toLowerCase() === 'select' && !frag.querySelector(id)) { - // generate a
- var selectdetails = document.createElement('details'), - selectsummary = document.createElement('summary'); - selectsummary.innerText = mapmlInput.getAttribute('name'); - L.DomUtil.addClass(selectdetails, 'mapml-control-layers'); - selectdetails.appendChild(selectsummary); - selectdetails.appendChild(mapmlInput.htmlselect); - frag.appendChild(selectdetails); - } - } + return (this._map) ? this._update() : this; + }, + _validateExtents: function (e) { + //the settimeout allows the function inside the {} to be moved to the task/callback queue rather than executing immediately + //allowing the callback function to be run after all the other moveend event handlers + setTimeout(()=>{ + let layerTypes = ["_staticTileLayer","_imageLayer","_mapmlvectors","_templatedLayer"],layerProjection; + for (let i = 0; i < this._layers.length; i++) { + let count = 0, total=0; + if(this._layers[i].layer._extent){ + layerProjection = this._layers[i].layer._extent.getAttribute('units') || + this._layers[i].layer._extent.getAttribute('content') || + this._layers[i].layer._extent.querySelector("input[type=projection]").getAttribute('value'); + } else { + layerProjection = FALLBACK_PROJECTION; } - } - details.appendChild(frag); - } - return label; - }, - _initExtent: function(content) { - if (!this._href && !content) {return;} - var layer = this; - // the this._href (comes from layer@src) should take precedence over - // content of the element, but if no this._href / src is provided - // but there *is* child content of the element (which is copied/ - // referred to by this._content), we should use that content. - if (this._href) { - var xhr = new XMLHttpRequest(); -// xhr.withCredentials = true; - _get(this._href, _processInitialExtent); - } else if (content) { - // may not set this._extent if it can't be done from the content - // (eg a single point) and there's no map to provide a default yet - _processInitialExtent.call(this, content); - } - function _get(url, fCallback ) { - xhr.onreadystatechange = function () { - if(this.readyState === this.DONE) { - if (this.status === 400 || - this.status === 404 || - this.status === 500 || - this.status === 406) { - layer.error = true; - layer.fire('extentload', layer, true); - xhr.abort(); - } - }}; - xhr.onload = fCallback; - xhr.onerror = function () { - layer.error = true; - layer.fire('extentload', layer, true); - }; - xhr.open("GET", url); - xhr.setRequestHeader("Accept",M.mime); - xhr.overrideMimeType("text/xml"); - xhr.send(); - } - function _processInitialExtent(content) { - var mapml = this.responseXML || content; - if (this.readyState === this.DONE && mapml) { - var serverExtent = mapml.querySelector('extent'), - projectionMatch = serverExtent && serverExtent.hasAttribute('units') && - serverExtent.getAttribute('units').toUpperCase() === layer.options.mapprojection, - selectedAlternate = !projectionMatch && mapml.querySelector('head link[rel=alternate][projection='+layer.options.mapprojection+']'), - - base = - (new URL(mapml.querySelector('base') ? mapml.querySelector('base').getAttribute('href') : null || this.responseURL, this.responseURL)).href; - - if (!serverExtent) { - serverExtent = layer._synthesizeExtent(mapml); - // the mapml resource does not have a (complete) extent form, save - // its content if any so we don't have to revisit the server, ever. - if (mapml.querySelector('feature,image,tile')) { - layer._content = mapml; - } - } else if (!projectionMatch && selectedAlternate && selectedAlternate.hasAttribute('href')) { - - layer.fire('changeprojection', {href: (new URL(selectedAlternate.getAttribute('href'), base)).href}, false); - return; - } else if (!serverExtent.hasAttribute("action") && - serverExtent.querySelector('link[rel=tile],link[rel=image],link[rel=features],link[rel=query]') && - serverExtent.hasAttribute("units")) { - layer._templateVars = []; - // set up the URL template and associated inputs (which yield variable values when processed) - var tlist = serverExtent.querySelectorAll('link[rel=tile],link[rel=image],link[rel=features],link[rel=query]'), - varNamesRe = (new RegExp('(?:\{)(.*?)(?:\})','g')), - zoomInput = serverExtent.querySelector('input[type="zoom" i]'), - includesZoom = false; - for (var i=0;i< tlist.length;i++) { - var t = tlist[i], - template = t.getAttribute('tref'), 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 = []; - while ((v = varNamesRe.exec(template)) !== null) { - var varName = v[1], - inp = serverExtent.querySelector('input[name='+varName+'],select[name='+varName+']'); - if (inp) { - inputs.push(inp); - includesZoom = inp.hasAttribute("type") && inp.getAttribute("type").toLowerCase() === "zoom"; - if (inp.hasAttribute('shard')) { - var id = inp.getAttribute('list'); - inp.servers = []; - var servers = serverExtent.querySelectorAll('datalist#'+id + ' > option'); - if (servers.length === 0 && inp.hasAttribute('value')) { - servers = inp.getAttribute('value').split(''); - } - for (var s=0;s < servers.length;s++) { - if (servers[s].getAttribute) { - inp.servers.push(servers[s].getAttribute('value')); - } else { - inp.servers.push(servers[s]); - } - } - } else if (inp.tagName.toLowerCase() === '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("select"); - // 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( !layerProjection || layerProjection === this._map.options.projection){ + for(let j = 0 ;j 1) { - var stylesControl = document.createElement('details'), - stylesControlSummary = document.createElement('summary'); - stylesControlSummary.innerText = 'style'; - stylesControl.appendChild(stylesControlSummary); - var changeStyle = function (e) { - layer.fire('changestyle', {src: e.target.getAttribute("data-href")}, false); - }; - - for (var j=0;j 0) { - layer._tileLayer._onMapMLProcessed(); - } - layer.fire('extentload', layer, true); - } - } + this._layerControlInputs.push(obj.input); + obj.input.layerId = L.stamp(obj.layer); + + L.DomEvent.on(obj.input, 'click', this._onInputClick, this); + // this is necessary because when there are several layers in the + // layer control, the response to the last one can be a long time + // after the info is first displayed, so we have to go back and + // verify the extent and legend for the layer to know whether to + // disable it , add the legend link etc. + obj.layer.on('extentload', this._validateExtents, this); + + this._overlaysList.appendChild(layercontrols); + return layercontrols; + } + }); + var mapMlLayerControl = function (layers, options) { + return new MapMLLayerControl(layers, options); + }; + + var MapMLFeatures = L.FeatureGroup.extend({ + /* + * M.MapML turns any MapML feature data into a Leaflet layer. Based on L.GeoJSON. + */ + initialize: function (mapml, options) { + + L.setOptions(this, options); + this._container = L.DomUtil.create('div','leaflet-layer', this.options.pane); + // must have leaflet-pane class because of new/changed rule in leaflet.css + // info: https://github.com/Leaflet/Leaflet/pull/4597 + L.DomUtil.addClass(this._container,'leaflet-pane mapml-vector-container'); + L.setOptions(this.options.renderer, {pane: this._container}); + this._layers = {}; + if (mapml) { + //needed to check if the feature is static or not, since this method is used by templated also + if(!mapml.querySelector('extent') && mapml.querySelector('feature')){ + this._features = {}; + this._staticFeature = true; + this.isVisible = true; //placeholder for when this actually gets updated in the future + this.nativeZoom = +this._getNativeZoom(mapml); + this.zoomBounds = this._getZoomBounds(mapml); + this.layerBounds = this._getLayerBounds(mapml); + L.extend(this.options, this.zoomBounds); + } + this.addData(mapml); + if(this._staticFeature){ + this._resetFeatures(this._clampZoom(this.options._leafletLayer._map.getZoom())); + + this.options._leafletLayer._map._addZoomLimit(this); + } + } + }, + + getEvents: function(){ + if(this._staticFeature){ + return { + 'moveend':this._handleMoveEnd, + }; } - function _parseLink(rel, xml) { - // depends on js-uri http://code.google.com/p/js-uri/ - // would be greate to depend on the URL standard and not a library - var baseEl = xml.querySelector('base'), - base = baseEl ? baseEl.getAttribute('href'):null, - baseUri = (new URL(base||xml.baseURI)).href, - link = xml.querySelector('link[rel='+rel+']'), - relLink = link?(new URL(link.getAttribute('href'),baseUri)).href:null; - return relLink; + return { + 'moveend':this._removeCSS + }; + }, + + _handleMoveEnd : function(){ + let mapZoom = this._map.getZoom(); + if(mapZoom > this.zoomBounds.maxZoom || mapZoom < this.zoomBounds.minZoom){ + this.clearLayers(); + this.isVisible = false; + return; } - }, - _createExtent: function () { - - var extent = document.createElement('extent'), - xminInput = document.createElement('input'), - yminInput = document.createElement('input'), - xmaxInput = document.createElement('input'), - ymaxInput = document.createElement('input'), - zoom = document.createElement('input'), - projection = document.createElement('input'); - - zoom.setAttribute('type','zoom'); - zoom.setAttribute('min','0'); - zoom.setAttribute('max','0'); - - xminInput.setAttribute('type','xmin'); - xminInput.setAttribute('min',''); - xminInput.setAttribute('max',''); - - yminInput.setAttribute('type','ymin'); - yminInput.setAttribute('min',''); - yminInput.setAttribute('max',''); - - xmaxInput.setAttribute('type','xmax'); - xmaxInput.setAttribute('min',''); - xmaxInput.setAttribute('max',''); + let clampZoom = this._clampZoom(mapZoom); + this._resetFeatures(clampZoom); + this.isVisible = this._layers && this.layerBounds && + this.layerBounds.overlaps( + M.pixelToPCRSBounds( + this._map.getPixelBounds(), + mapZoom,this._map.options.projection)); + this._removeCSS(); + }, - ymaxInput.setAttribute('type','ymax'); - ymaxInput.setAttribute('min',''); - ymaxInput.setAttribute('max',''); - - projection.setAttribute('type','projection'); - projection.setAttribute('value','WGS84'); - - extent.setAttribute('action','synthetic'); - extent.appendChild(xminInput); - extent.appendChild(yminInput); - extent.appendChild(xmaxInput); - extent.appendChild(ymaxInput); - extent.appendChild(zoom); - extent.appendChild(projection); - - return extent; - }, - _validateExtent: function () { - // TODO: change so that the _extent bounds are set based on inputs - var serverExtent = this._extent; - if (!serverExtent || !serverExtent.querySelector || !this._map) { - return; + //sets default if any are missing, better to only replace ones that are missing + _getLayerBounds : function(container) { + if (!container) return null; + try{ + let projection = container.querySelector('meta[name=projection]') && + M.metaContentToObject( + container.querySelector('meta[name=projection]').getAttribute('content')) + .content.toUpperCase() || FALLBACK_PROJECTION; + let zoom = this._getNativeZoom(container); + let meta = container.querySelector('meta[name=extent]') && + M.metaContentToObject( + container.querySelector('meta[name=extent]').getAttribute('content')) || + {"top-left-vertical":0,"top-left-horizontal":0,"bottom-right-vertical":5,"bottom-right-horizontal":5}; + let cs = meta.cs || FALLBACK_CS; + return M.boundsToPCRSBounds( + L.bounds(L.point(+meta["top-left-vertical"],+meta["top-left-horizontal"]), + L.point(+meta["bottom-right-vertical"],+meta["bottom-right-horizontal"])), + zoom,projection,cs); + } catch (error){ + //if error then by default set the layer to osm and bounds to the entire map view + return M.boundsToPCRSBounds(M[FALLBACK_PROJECTION].options.crs.tilematrix.bounds(0),0,FALLBACK_PROJECTION,FALLBACK_CS); } - if (serverExtent.querySelector('[type=xmin][min=""], [type=xmin][max=""], [type=xmax][min=""], [type=xmax][max=""], [type=ymin][min=""], [type=ymin][max=""]')) { - var xmin = serverExtent.querySelector('[type=xmin]'), - ymin = serverExtent.querySelector('[type=ymin]'), - xmax = serverExtent.querySelector('[type=xmax]'), - ymax = serverExtent.querySelector('[type=ymax]'), - proj = serverExtent.querySelector('[type=projection][value]'), - bounds, projection; - if (proj) { - projection = proj.getAttribute('value'); - if (projection && projection === 'WGS84') { - bounds = this._map.getBounds(); - xmin.setAttribute('min',bounds.getWest()); - xmin.setAttribute('max',bounds.getEast()); - ymin.setAttribute('min',bounds.getSouth()); - ymin.setAttribute('max',bounds.getNorth()); - xmax.setAttribute('min',bounds.getWest()); - xmax.setAttribute('max',bounds.getEast()); - ymax.setAttribute('min',bounds.getSouth()); - ymax.setAttribute('max',bounds.getNorth()); - } else if (projection) { - // needs testing. Also, this will likely be - // messing with a server-generated extent. - bounds = this._map.getPixelBounds(); - xmin.setAttribute('min',bounds.getBottomLeft().x); - xmin.setAttribute('max',bounds.getTopRight().x); - ymin.setAttribute('min',bounds.getTopRight().y); - ymin.setAttribute('max',bounds.getBottomLeft().y); - xmax.setAttribute('min',bounds.getBottomLeft().x); - xmax.setAttribute('max',bounds.getTopRight().x); - ymax.setAttribute('min',bounds.getTopRight().y); - ymax.setAttribute('max',bounds.getBottomLeft().y); - } - } else { - this.error = true; - } + }, + _resetFeatures : function (zoom){ + this.clearLayers(); + if(this._features && this._features[zoom]){ + for(let k =0;k < this._features[zoom].length;k++){ + this.addLayer(this._features[zoom][k]); + } } - if (serverExtent.querySelector('[type=zoom][min=""], [type=zoom][max=""]')) { - var zoom = serverExtent.querySelector('[type=zoom]'); - zoom.setAttribute('min',this._map.getMinZoom()); - zoom.setAttribute('max',this._map.getMaxZoom()); + }, + + _clampZoom : function(zoom){ + if(zoom > this.zoomBounds.maxZoom || zoom < this.zoomBounds.minZoom) return zoom; + if (undefined !== this.zoomBounds.minNativeZoom && zoom < this.zoomBounds.minNativeZoom) { + return this.zoomBounds.minNativeZoom; } - var lp = serverExtent.hasAttribute("units") ? serverExtent.getAttribute("units") : null; - if (lp && lp === "OSMTILE" || lp === "WGS84" || lp === "APSTILE" || lp === "CBMTILE") { - this.crs = M[lp]; - } else { - this.crs = M.OSMTILE; + if (undefined !== this.zoomBounds.maxNativeZoom && this.zoomBounds.maxNativeZoom < zoom) { + return this.zoomBounds.maxNativeZoom; } - }, - _getMapMLExtent: function (bounds, zooms, proj) { - - var extent = this._createExtent(), - zoom = extent.querySelector('input[type=zoom]'), - xminInput = extent.querySelector('input[type=xmin]'), - yminInput = extent.querySelector('input[type=ymin]'), - xmaxInput = extent.querySelector('input[type=xmax]'), - ymaxInput = extent.querySelector('input[type=ymax]'), - projection = extent.querySelector('input[type=projection]'), - zmin = zooms[0] !== undefined && zooms[1] !== undefined ? Math.min(zooms[0],zooms[1]) : '', - zmax = zooms[0] !== undefined && zooms[1] !== undefined ? Math.max(zooms[0],zooms[1]) : '', - xmin = bounds ? bounds._southWest ? bounds.getWest() : bounds.getBottomLeft().x : '', - ymin = bounds ? bounds._southWest ? bounds.getSouth() : bounds.getTopRight().y : '', - xmax = bounds ? bounds._southWest ? bounds.getEast() : bounds.getTopRight().x : '', - ymax = bounds ? bounds._southWest ? bounds.getNorth() : bounds.getBottomLeft().y : ''; - - zoom.setAttribute('min', typeof(zmin) === 'number' && isNaN(zmin)? '' : zmin); - zoom.setAttribute('max', typeof(zmax) === 'number' && isNaN(zmax)? '' : zmax); - - xminInput.setAttribute('min',xmin); - xminInput.setAttribute('max',xmax); - - yminInput.setAttribute('min',ymin); - yminInput.setAttribute('max',ymax); - - xmaxInput.setAttribute('min',xmin); - xmaxInput.setAttribute('max',xmax); - ymaxInput.setAttribute('min',ymin); - ymaxInput.setAttribute('max',ymax); - - projection.setAttribute('value',bounds && bounds._southWest && !proj ? 'WGS84' : proj); + return zoom; + }, - return extent; - }, - _synthesizeExtent: function (mapml) { - var metaZoom = mapml.querySelectorAll('meta[name=zoom]')[0], - metaExtent = mapml.querySelector('meta[name=extent]'), - metaProjection = mapml.querySelector('meta[name=projection]'), - proj = metaProjection ? metaProjection.getAttribute('content'): 'WGS84', - i,expressions,bounds,zmin,zmax,xmin,ymin,xmax,ymax,expr,lhs,rhs; - if (metaZoom) { - expressions = metaZoom.getAttribute('content').split(','); - for (i=0;i nMax) nMax = +features[i].getAttribute('zoom'); + if(+features[i].getAttribute('zoom') < nMin) nMin = +features[i].getAttribute('zoom'); } - return this._getMapMLExtent(bounds, [zmin,zmax], proj); - }, - // a layer must share a projection with the map so that all the layers can - // be overlayed in one coordinate space. WGS84 is a 'wildcard', sort of. - getProjection: function () { - // TODO review logic because input[type=projection] is deprecated - if (!this._extent || !this._extent.querySelector('input[type=projection]')) return 'WGS84'; - var projection = this._extent.querySelector('input[type=projection]'); - if (!projection.getAttribute('value')) return 'WGS84'; - return projection.getAttribute('value'); - }, - _parseLicenseAndLegend: function (xml, layer) { - var licenseLink = xml.querySelector('link[rel=license]'), licenseTitle, licenseUrl, attText; - if (licenseLink) { - licenseTitle = licenseLink.getAttribute('title'); - licenseUrl = licenseLink.getAttribute('href'); - attText = ''+licenseTitle+''; - } - L.setOptions(layer,{attribution:attText}); - var legendLink = xml.querySelector('link[rel=legend]'); - if (legendLink) { - layer._legendUrl = legendLink.getAttribute('href'); + try{ + projection = M.metaContentToObject(container.querySelector('meta[name=projection]').getAttribute('content')).content; + meta = M.metaContentToObject(container.querySelector('meta[name=zoom]').getAttribute('content')); + } catch(error){ + return { + minZoom:0, + maxZoom: M[projection || FALLBACK_PROJECTION].options.resolutions.length - 1, + minNativeZoom:nMin, + maxNativeZoom:nMax + }; } - }, - _onMoveEnd: function () { - // this can only be done when the layer is on a map, because the url - // calculation requires to process the extent of the map through the - // extent form that should have already been received. - var url = this._calculateUrl(); - if (url) { - this.href = url; - this.fire('loadstart'); - this._mapmlvectors.clearLayers(); - this._initEl(); - this._getMapML(url); - } else if (this._content && !this._mapmlvectors.getLayers().length) { - // if the content hasn't been parsed yet, parse it into vectors, - // images and tiles, if applicable - // - // this shouldn't only be contingent on vectors - could be other stuff - // located in this._content that has been parsed, so it should be - // generalized to be: if (this._content && !vectors && !images && !tiles) - // tiles could be = Object.keys($0._layer._tileLayer._tiles).length - // images = not sure yet. NOT FINISHED. - // vectors = this._mapmlvectors.getLayers().length - this._getMapML(null); + return { + minZoom:+meta.min , + maxZoom:+meta.max , + minNativeZoom:nMin, + maxNativeZoom:nMax + }; + }, + + addData: function (mapml) { + var features = mapml.nodeType === Node.DOCUMENT_NODE || mapml.nodeName === "LAYER-" ? mapml.getElementsByTagName("feature") : null, + i, len, feature; + + var linkedStylesheets = mapml.nodeType === Node.DOCUMENT_NODE ? mapml.querySelector("link[rel=stylesheet],style") : null; + if (linkedStylesheets) { + var base = mapml.querySelector('base') && mapml.querySelector('base').hasAttribute('href') ? + new URL(mapml.querySelector('base').getAttribute('href')).href : + mapml.URL; + M.parseStylesheetAsHTML(mapml,base,this._container); } - }, - _initEl: function () { - if (!this._mapmlTileContainer) {return;} - var container = this._mapmlTileContainer; - while (container.firstChild) - container.removeChild(container.firstChild); - }, - _reset: function() { - this._initEl(); - L.empty(this._container); - this._mapmlvectors.clearLayers(); - //this._map.removeLayer(this._mapmlvectors); - return; - }, - // return the LatLngBounds of the map unprojected such that the whole - // map is covered, not just a band defined by the projected map bounds. - _getUnprojectedMapLatLngBounds: function(map) { - - map = map||this._map; - var origin = map.getPixelOrigin(), - bounds = map.getPixelBounds(), - nw = map.unproject(origin), - sw = map.unproject(bounds.getBottomLeft()), - ne = map.unproject(bounds.getTopRight()), - se = map.unproject(origin.add(map.getSize())); - return L.latLngBounds(sw,ne).extend(se).extend(nw); - }, - _calculateUrl: function() { - - if (!this._map) return null; - if (!this._mapmlTileContainer && !this._extent) return this._href; - var extent = this._extent; - if (!extent) return this._href; - // action SHOULD HAVE BEEN resolved against any base ALREADY - var action = extent.getAttribute("action"), - base = (new URL(this._href)).href; - // establish the range of zoom values for the extent - var zoom = extent.querySelectorAll("input[type=zoom]")[0]; - if ( !zoom ) return null; - var min = parseInt(zoom.getAttribute("min")), - max = parseInt(zoom.getAttribute("max")), - values = {}, // the values object will contain the values for the URI template - mapZoom = this._map.getZoom(); - // check that the zoom of the map is in the range of the zoom of the service - if ( min <= mapZoom && mapZoom <= max) { - values.zoom = mapZoom; - } else if (!action){ - if (extent.zoomin && mapZoom > max) { - this._href = extent.zoomin; - this.href = extent.zoomin; - return this.href; - } else if (extent.zoomout && mapZoom < min) { - this._href = extent.zoomout; - this.href = extent.zoomout; - return this.href; + if (features) { + for (i = 0, len = features.length; i < len; i++) { + // Only add this if geometry is set and not null + feature = features[i]; + var geometriesExist = feature.getElementsByTagName("geometry").length && feature.getElementsByTagName("coordinates").length; + if (geometriesExist) { + this.addData(feature); } - } else { - return null; + } + return this; //if templated this runs } + + //if its a mapml with no more links this runs + var options = this.options; + + if (options.filter && !options.filter(mapml)) { return; } - if (!action || action === "synthetic") return null; - var b,projectionValue = extent.getAttribute('units'); - - // if the mapml extent being processed is WGS84, we need to speak in those units - if (projectionValue === 'WGS84') { - b = this._getUnprojectedMapLatLngBounds(); - } else { - // otherwise, use the bounds of the map - b = this._map.getPixelBounds(); + if (mapml.classList.length) { + options.className = mapml.classList.value; } - var varnames = this._setUpInputVars(extent.querySelectorAll('input')); - - var queryParams = ""; - values[varnames.extent.zoom.name] = mapZoom; - queryParams += varnames.extent.zoom.name+"={"+varnames.extent.zoom.name+"}&"; - values[varnames.extent.left.name] = b.min?b.min.x:b.getWest(); - queryParams += varnames.extent.left.name+"={"+varnames.extent.left.name+"}&"; - values[varnames.extent.bottom.name] = b.max?b.max.y:b.getSouth(); - queryParams += varnames.extent.bottom.name+"={"+varnames.extent.bottom.name+"}&"; - values[varnames.extent.right.name] = b.max?b.max.x:b.getEast(); - queryParams += varnames.extent.right.name+"={"+varnames.extent.right.name+"}&"; - values[varnames.extent.top.name] = b.min?b.min.y:b.getNorth(); - queryParams += varnames.extent.top.name+"={"+varnames.extent.top.name+"}"+(varnames.extent.hidden.length>0?"&":""); - for (var i=0;i