diff --git a/Gruntfile.js b/Gruntfile.js index 41145842b..e4edb69fe 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -29,6 +29,14 @@ module.exports = function(grunt) { 'dist/map-caption.js': ['src/map-caption.js'], 'dist/map-feature.js': ['src/map-feature.js'], 'dist/map-extent.js': ['src/map-extent.js'], + 'dist/map-input.js': ['src/map-input.js'], + 'dist/map-link.js': ['src/map-link.js'], + // temporary, will be bundled into mapml.js possibly + 'dist/zoomInput.js': ['src/mapml/elementSupport/inputs/zoomInput.js'], + 'dist/hiddenInput.js': ['src/mapml/elementSupport/inputs/hiddenInput.js'], + 'dist/widthInput.js': ['src/mapml/elementSupport/inputs/widthInput.js'], + 'dist/heightInput.js': ['src/mapml/elementSupport/inputs/heightInput.js'], + 'dist/locationInput.js': ['src/mapml/elementSupport/inputs/locationInput.js'], 'dist/map-area.js': ['src/map-area.js'], 'dist/layer.js': ['src/layer.js'], 'dist/leaflet.js': ['dist/leaflet-src.js', diff --git a/index.html b/index.html index ddfa9d00d..cbf7b56a3 100644 --- a/index.html +++ b/index.html @@ -23,9 +23,9 @@ /* Responsive map. */ max-width: 100%; - /* Full viewport. */ + /* Full viewport. width: 100%; - height: 100%; + height: 100%; */ /* Remove default (native-like) border. */ border: none; @@ -72,58 +72,72 @@ - - A pleasing map of Canada - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
code1200020
accuracy26
valdate1995
image - - -
themeFO
type2
elevation61
altiaccu5
-
- - - -75.705278 45.397778 - - -
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/map-input.js b/src/map-input.js index bd92e01b1..d97ca3c2d 100644 --- a/src/map-input.js +++ b/src/map-input.js @@ -1,4 +1,8 @@ -import { MapLink } from './map-link.js'; +import { ZoomInput } from './zoomInput.js'; +import { HiddenInput } from './hiddenInput.js'; +import { WidthInput } from './widthInput.js'; +import { HeightInput } from './heightInput.js'; +import { LocationInput } from './locationInput.js'; export class MapInput extends HTMLElement { static get observedAttributes() { @@ -33,7 +37,7 @@ export class MapInput extends HTMLElement { } } get value() { - return this.getAttribute('value'); + return this.input.getValue(); } set value(val) { if (val) { @@ -73,7 +77,26 @@ export class MapInput extends HTMLElement { } } get min() { - return this.getAttribute('min'); + if ( + this.type === 'height' || + this.type === 'width' || + this.type === 'hidden' + ) { + return null; + } + if (this.getAttribute('min')) { + return this.getAttribute('min'); + } else if (this._layer._layerEl.querySelector('map-meta[name=zoom]')) { + // fallback map-meta on layer + return M._metaContentToObject( + this._layer._layerEl + .querySelector('map-meta[name=zoom]') + .getAttribute('content') + ).min; + } else { + // fallback map min + return this._layer._layerEl.extent.zoom.minZoom.toString(); + } } set min(val) { if (val) { @@ -81,7 +104,26 @@ export class MapInput extends HTMLElement { } } get max() { - return this.getAttribute('max'); + if ( + this.type === 'height' || + this.type === 'width' || + this.type === 'hidden' + ) { + return null; + } + if (this.getAttribute('max')) { + return this.getAttribute('max'); + } else if (this._layer._layerEl.querySelector('map-meta[name=zoom]')) { + // fallback map-meta on layer + return M._metaContentToObject( + this._layer._layerEl + .querySelector('map-meta[name=zoom]') + .getAttribute('content') + ).max; + } else { + // fallback map max + return this._layer._layerEl.extent.zoom.maxZoom.toString(); + } } set max(val) { if (val) { @@ -89,7 +131,11 @@ export class MapInput extends HTMLElement { } } get step() { - return this.getAttribute('step'); + if (this.type !== 'zoom') { + return null; + } else { + return this.getAttribute('step') || '1'; + } } set step(val) { if (val) { @@ -100,52 +146,67 @@ export class MapInput extends HTMLElement { switch (name) { case 'name': if (oldValue !== newValue) { - // handle side effects + // update associated class value on attribute change + if (oldValue !== null) { + this.input.name = newValue; + } } break; case 'type': if (oldValue !== newValue) { // handle side effects + // not allowed to change 'type' } break; case 'value': if (oldValue !== newValue) { - // handle side effects + if (oldValue !== null) { + this.input.value = newValue; + } else { + this.initialValue = newValue; + } } break; case 'axis': - if (oldValue !== newValue) { + if (oldValue !== newValue && this.input) { // handle side effects + this.input.axis = newValue; } break; case 'units': - if (oldValue !== newValue) { + if (oldValue !== newValue && this.input) { // handle side effects + this.input.units = newValue; } break; case 'position': - if (oldValue !== newValue) { + if (oldValue !== newValue && this.input) { // handle side effects + this.input.position = newValue; } break; case 'rel': - if (oldValue !== newValue) { + if (oldValue !== newValue && this.input) { // handle side effects + this.input.rel = newValue; } break; case 'min': - if (oldValue !== newValue) { + if (oldValue !== newValue && this.input) { // handle side effects + this.input.min = newValue; } break; case 'max': - if (oldValue !== newValue) { + if (oldValue !== newValue && this.input) { // handle side effects + this.input.max = newValue; } break; case 'step': - if (oldValue !== newValue) { + if (oldValue !== newValue && this.input) { // handle side effects + this.input.step = newValue; } break; } @@ -154,7 +215,81 @@ export class MapInput extends HTMLElement { // Always call super first in constructor super(); } - connectedCallback() {} + connectedCallback() { + if (this.parentElement.nodeName === 'MAP-EXTENT') { + this._layer = this.parentElement._layer; + } + switch (this.type) { + case 'zoom': + // input will store the input Class specific to the input type + this.input = new ZoomInput( + this.name, + this.min, + this.max, + this.initialValue, + this.step, + this._layer + ); + break; + case 'location': + // input will store the input Class specific to the input type + this.input = new LocationInput( + this.name, + this.position, + this.axis, + this.units, + this.min, + this.max, + this.rel, + this._layer + ); + break; + case 'width': + // input will store the input Class specific to the input type + this.input = new WidthInput(this.name, this._layer); + break; + case 'height': + // input will store the input Class specific to the input type + this.input = new HeightInput(this.name, this._layer); + break; + case 'hidden': + // input will store the input Class specific to the input type + this.input = new HiddenInput(this.name, this.initialValue); + break; + } + } disconnectedCallback() {} + + //https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity + checkValidity() { + if (this.input.validateInput()) { + return true; + } else { + const evt = new Event('invalid', { + bubbles: true, + cancelable: true, + composed: true + }); + this.dispatchEvent(evt); + return false; + } + } + + //https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity + reportValidity() { + if (this.input.validateInput()) { + return true; + } else { + const evt = new Event('invalid', { + bubbles: true, + cancelable: true, + composed: true + }); + this.dispatchEvent(evt); + //if the event isn't canceled reports the problem to the user. + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-reportvalidity-dev + console.log("Input type='" + this.type + "' is not valid!"); + return false; + } + } } -window.customElements.define('map-input', MapInput); diff --git a/src/map-link.js b/src/map-link.js index 969bf7a41..2ebf0ed70 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -149,5 +149,30 @@ export class MapLink extends HTMLElement { } connectedCallback() {} disconnectedCallback() {} + + // Resolve the templated URL with info from the sibling map-input's + resolve() { + if (this.tref) { + let obj = {}; + const inputs = this.parentElement.querySelectorAll('map-input'); + if (this.rel === 'image') { + // image/map + for (let i = 0; i < inputs.length; i++) { + const inp = inputs[i]; + obj[inp.name] = inp.value; + } + console.log(obj); // DEBUGGING + return L.Util.template(this.tref, obj); + } else if (this.rel === 'tile') { + // TODO. Need to get tile coords from moveend + // should be done/called from the TemplatedTilelayer.js file + return obj; + } else if (this.rel === 'query') { + // TODO. Need to get the click coords from click event + // should be done/called from the templatedlayer.js file + } else if (this.rel === 'features') { + // TODO. + } + } + } } -window.customElements.define('map-link', MapLink); diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 1210f4b72..e551569ab 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -5,6 +5,8 @@ import { MapLayer } from './layer.js'; import { MapCaption } from './map-caption.js'; import { MapFeature } from './map-feature.js'; import { MapExtent } from './map-extent.js'; +import { MapInput } from './map-input.js'; +import { MapLink } from './map-link.js'; export class MapViewer extends HTMLElement { static get observedAttributes() { @@ -1304,3 +1306,5 @@ window.customElements.define('layer-', MapLayer); window.customElements.define('map-caption', MapCaption); window.customElements.define('map-feature', MapFeature); window.customElements.define('map-extent', MapExtent); +window.customElements.define('map-input', MapInput); +window.customElements.define('map-link', MapLink); diff --git a/src/mapml/elementSupport/inputs/heightInput.js b/src/mapml/elementSupport/inputs/heightInput.js new file mode 100644 index 000000000..3629b0ddd --- /dev/null +++ b/src/mapml/elementSupport/inputs/heightInput.js @@ -0,0 +1,18 @@ +export class HeightInput { + constructor(name, layer) { + this.name = name; + this.layer = layer; + } + + validateInput() { + // name is required + if (!this.name) { + return false; + } + return true; + } + + getValue() { + return this.layer._map.getSize().y; + } +} diff --git a/src/mapml/elementSupport/inputs/hiddenInput.js b/src/mapml/elementSupport/inputs/hiddenInput.js new file mode 100644 index 000000000..0919ef90f --- /dev/null +++ b/src/mapml/elementSupport/inputs/hiddenInput.js @@ -0,0 +1,19 @@ +export class HiddenInput { + constructor(name, value) { + this.name = name; + this.value = value; + } + + validateInput() { + // name is required + // value is required + if (!this.name || !this.value) { + return false; + } + return true; + } + + getValue() { + return this.value; + } +} diff --git a/src/mapml/elementSupport/inputs/locationInput.js b/src/mapml/elementSupport/inputs/locationInput.js new file mode 100644 index 000000000..fb4e7b880 --- /dev/null +++ b/src/mapml/elementSupport/inputs/locationInput.js @@ -0,0 +1,116 @@ +export class LocationInput { + constructor(name, position, axis, units, min, max, rel, layer) { + this.name = name; + this.position = position; + this.axis = axis; + // if unit/cs not present, find it + if (!units && axis && !['i', 'j'].includes(axis)) { + this.units = M.axisToCS(axis).toLowerCase(); + } else { + this.units = units; // cs + } + this.min = min; + this.max = max; + this.rel = rel; + this.layer = layer; + } + + validateInput() { + // name is required + // axis is required + if (!this.name || !this.axis) { + return false; + } + // cs/units is only required when the axis is i/j. To differentiate between the units/cs + if ( + (this.axis === 'i' || this.axis === 'j') && + !['map', 'tile'].includes(this.units) + ) { + return false; + } + // check if axis match the units/cs + if (this.units) { + let axisCS = M.axisToCS(this.axis); + if ( + typeof axisCS === 'string' && + axisCS.toUpperCase() !== this.units.toUpperCase() + ) { + return false; + } + } + // position is not required, will default to top-left + // min max fallbacks, map-meta -> projection + // rel not required, default is image/extent + return true; + } + + _TCRSToPCRS(coords, zoom) { + // TCRS pixel point to Projected CRS point (in meters, presumably) + var map = this.layer._map, + crs = map.options.crs, + loc = crs.transformation.untransform(coords, crs.scale(zoom)); + return loc; + } + + getValue(zoom = undefined, bounds = undefined) { + // units = cs + // + if (zoom === undefined) zoom = this.layer._map.getZoom(); + if (bounds === undefined) bounds = this.layer._map.getPixelBounds(); + + if (this.units === 'pcrs' || this.units === 'gcrs') { + switch (this.axis) { + case 'longitude': + case 'easting': + if (this.position) { + if (this.position.match(/.*?-left/i)) { + return this._TCRSToPCRS(bounds.min, zoom).x; + } else if (this.position.match(/.*?-right/i)) { + return this._TCRSToPCRS(bounds.max, zoom).x; + } + } else { + // position is not required, will default to top-left + return this._TCRSToPCRS(bounds.min, zoom).x; + } + break; + case 'latitude': + case 'northing': + if (this.position) { + if (this.position.match(/top-.*?/i)) { + return this._TCRSToPCRS(bounds.min, zoom).y; + } else if (this.position.match(/bottom-.*?/i)) { + return this._TCRSToPCRS(bounds.max, zoom).y; + } + } else { + // position is not required, will default to top-left + return this._TCRSToPCRS(bounds.min, zoom).y; + } + break; + } + } else if (this.units === 'tilematrix') { + // Value is retrieved from the createTile method of TemplatedTileLayer, on move end. + // Different values for each tile when filling in the map tiles on the map. + // Currently storing all x,y,z within one object, + // TODO: change return value as needed based on usage by map-input + // https://github.com/Leaflet/Leaflet/blob/6994baf25f267db1c8b720c28a61e0700d0aa0e8/src/layer/tile/GridLayer.js#L652 + const center = this.layer._map.getCenter(); + const templatedTileLayer = this.layer._templatedLayer._templates[0].layer; + const pixelBounds = templatedTileLayer._getTiledPixelBounds(center); + const tileRange = templatedTileLayer._pxBoundsToTileRange(pixelBounds); + let obj = []; + for (let j = tileRange.min.y; j <= tileRange.max.y; j++) { + for (let i = tileRange.min.x; i <= tileRange.max.x; i++) { + const coords = new L.Point(i, j); + coords.z = templatedTileLayer._tileZoom; + obj.push(coords); + } + } + return obj; + } else if (this.units === 'tile' || this.units === 'map') { + // used for query handler on map enter or click. + // mapi, tilei, mapj, tilej used for query handling, value is derived from the mouse click event + // or center of the map when used with keyboard. + } + return; + } +} diff --git a/src/mapml/elementSupport/inputs/widthInput.js b/src/mapml/elementSupport/inputs/widthInput.js new file mode 100644 index 000000000..756f72ffe --- /dev/null +++ b/src/mapml/elementSupport/inputs/widthInput.js @@ -0,0 +1,18 @@ +export class WidthInput { + constructor(name, layer) { + this.name = name; + this.layer = layer; + } + + validateInput() { + // name is required + if (!this.name) { + return false; + } + return true; + } + + getValue() { + return this.layer._map.getSize().x; + } +} diff --git a/src/mapml/elementSupport/inputs/zoomInput.js b/src/mapml/elementSupport/inputs/zoomInput.js new file mode 100644 index 000000000..5ac511675 --- /dev/null +++ b/src/mapml/elementSupport/inputs/zoomInput.js @@ -0,0 +1,26 @@ +export class ZoomInput { + constructor(name, min, max, value, step, layer) { + this.name = name; + this.min = min; + this.max = max; + this.value = value; + this.step = step; + this.layer = layer; + } + + validateInput() { + // name is required + if (!this.name) { + return false; + } + // min and max can not be present + // fallback would be layer's meta, -> projection min, max + // don't need value, map-meta max value, -> fallback is max zoom of projection + // don't need step, defaults to 1 + return true; + } + + getValue() { + return this.layer._map.options.mapEl.zoom; + } +} diff --git a/src/web-map.js b/src/web-map.js index ff03f80ae..cc2154725 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -6,6 +6,8 @@ import { MapArea } from './map-area.js'; import { MapCaption } from './map-caption.js'; import { MapFeature } from './map-feature.js'; import { MapExtent } from './map-extent.js'; +import { MapInput } from './map-input.js'; +import { MapLink } from './map-link.js'; export class WebMap extends HTMLMapElement { static get observedAttributes() { @@ -1371,3 +1373,5 @@ window.customElements.define('map-area', MapArea, { extends: 'area' }); window.customElements.define('map-caption', MapCaption); window.customElements.define('map-feature', MapFeature); window.customElements.define('map-extent', MapExtent); +window.customElements.define('map-input', MapInput); +window.customElements.define('map-link', MapLink);