From d5813deb081d6ed39642fc17704595125dc972e5 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 21 Sep 2021 00:06:26 +0000 Subject: [PATCH] Allow maps to be configured with a promise for view props --- examples/cog-math-multisource.js | 7 +- examples/cog-math.js | 46 ++++-------- examples/cog-overviews.js | 75 ++++++++----------- examples/cog.js | 32 +++----- src/ol/PluggableMap.js | 27 ++++++- src/ol/source/GeoTIFF.js | 56 ++++++++++++-- src/ol/source/Source.js | 29 +++++++ .../spec/ol/{map.test.js => Map.test.js} | 62 ++++++++++++++- test/browser/spec/ol/source/geotiff.test.js | 12 +++ 9 files changed, 230 insertions(+), 116 deletions(-) rename test/browser/spec/ol/{map.test.js => Map.test.js} (95%) diff --git a/examples/cog-math-multisource.js b/examples/cog-math-multisource.js index 05da37fc43..6ef05a4329 100644 --- a/examples/cog-math-multisource.js +++ b/examples/cog-math-multisource.js @@ -1,7 +1,6 @@ import GeoTIFF from '../src/ol/source/GeoTIFF.js'; import Map from '../src/ol/Map.js'; import TileLayer from '../src/ol/layer/WebGLTile.js'; -import View from '../src/ol/View.js'; const source = new GeoTIFF({ sources: [ @@ -57,9 +56,5 @@ const map = new Map({ source, }), ], - view: new View({ - center: [1900000, 6100000], - zoom: 13, - minZoom: 10, - }), + view: source.getView(), }); diff --git a/examples/cog-math.js b/examples/cog-math.js index 0b2b8869f5..1438e47637 100644 --- a/examples/cog-math.js +++ b/examples/cog-math.js @@ -1,16 +1,21 @@ import GeoTIFF from '../src/ol/source/GeoTIFF.js'; import Map from '../src/ol/Map.js'; import TileLayer from '../src/ol/layer/WebGLTile.js'; -import View from '../src/ol/View.js'; -import proj4 from 'proj4'; -import {getCenter} from '../src/ol/extent.js'; -import {register} from '../src/ol/proj/proj4.js'; -proj4.defs('EPSG:32636', '+proj=utm +zone=36 +datum=WGS84 +units=m +no_defs'); -register(proj4); - -// metadata from https://s3.us-west-2.amazonaws.com/sentinel-cogs/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/S2A_36QWD_20200701_0_L2A.json -const sourceExtent = [499980, 1790220, 609780, 1900020]; +const source = new GeoTIFF({ + sources: [ + { + // visible red, band 1 in the style expression above + url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/B04.tif', + max: 10000, + }, + { + // near infrared, band 2 in the style expression above + url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/B08.tif', + max: 10000, + }, + ], +}); const map = new Map({ target: 'map', @@ -69,27 +74,8 @@ const map = new Map({ [0, 69, 0], ], }, - source: new GeoTIFF({ - sources: [ - { - // visible red, band 1 in the style expression above - url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/B04.tif', - max: 10000, - }, - { - // near infrared, band 2 in the style expression above - url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/B08.tif', - max: 10000, - }, - ], - }), - extent: sourceExtent, + source: source, }), ], - view: new View({ - projection: 'EPSG:32636', - center: getCenter(sourceExtent), - extent: sourceExtent, - zoom: 9, - }), + view: source.getView(), }); diff --git a/examples/cog-overviews.js b/examples/cog-overviews.js index 945b7a00f5..872f7b6b76 100644 --- a/examples/cog-overviews.js +++ b/examples/cog-overviews.js @@ -1,62 +1,49 @@ import GeoTIFF from '../src/ol/source/GeoTIFF.js'; import Map from '../src/ol/Map.js'; import TileLayer from '../src/ol/layer/WebGLTile.js'; -import View from '../src/ol/View.js'; -import proj4 from 'proj4'; -import {getCenter} from '../src/ol/extent.js'; -import {register} from '../src/ol/proj/proj4.js'; - -proj4.defs('EPSG:32645', '+proj=utm +zone=45 +datum=WGS84 +units=m +no_defs'); -register(proj4); - -const sourceExtent = [382200, 2279370, 610530, 2512500]; - -const base = - 'https://landsat-pds.s3.amazonaws.com/c1/L8/139/045/LC08_L1TP_139045_20170304_20170316_01_T1/LC08_L1TP_139045_20170304_20170316_01_T1'; // scale values in this range to 0 - 1 const min = 10000; const max = 15000; +const base = + 'https://landsat-pds.s3.amazonaws.com/c1/L8/139/045/LC08_L1TP_139045_20170304_20170316_01_T1/LC08_L1TP_139045_20170304_20170316_01_T1'; + +const source = new GeoTIFF({ + sources: [ + { + url: `${base}_B6.TIF`, + overviews: [`${base}_B6.TIF.ovr`], + min: min, + max: max, + nodata: 0, + }, + { + url: `${base}_B5.TIF`, + overviews: [`${base}_B5.TIF.ovr`], + min: min, + max: max, + nodata: 0, + }, + { + url: `${base}_B3.TIF`, + overviews: [`${base}_B3.TIF.ovr`], + min: min, + max: max, + nodata: 0, + }, + ], +}); + const map = new Map({ target: 'map', layers: [ new TileLayer({ - extent: sourceExtent, style: { saturation: -0.3, }, - source: new GeoTIFF({ - sources: [ - { - url: `${base}_B6.TIF`, - overviews: [`${base}_B6.TIF.ovr`], - min: min, - max: max, - nodata: 0, - }, - { - url: `${base}_B5.TIF`, - overviews: [`${base}_B5.TIF.ovr`], - min: min, - max: max, - nodata: 0, - }, - { - url: `${base}_B3.TIF`, - overviews: [`${base}_B3.TIF.ovr`], - min: min, - max: max, - nodata: 0, - }, - ], - }), + source: source, }), ], - view: new View({ - projection: 'EPSG:32645', - center: getCenter(sourceExtent), - extent: sourceExtent, - zoom: 8, - }), + view: source.getView(), }); diff --git a/examples/cog.js b/examples/cog.js index 24a1038021..bcff3e80c7 100644 --- a/examples/cog.js +++ b/examples/cog.js @@ -1,35 +1,21 @@ import GeoTIFF from '../src/ol/source/GeoTIFF.js'; import Map from '../src/ol/Map.js'; import TileLayer from '../src/ol/layer/WebGLTile.js'; -import View from '../src/ol/View.js'; -import proj4 from 'proj4'; -import {getCenter} from '../src/ol/extent.js'; -import {register} from '../src/ol/proj/proj4.js'; -proj4.defs('EPSG:32636', '+proj=utm +zone=36 +datum=WGS84 +units=m +no_defs'); -register(proj4); - -// metadata from https://s3.us-west-2.amazonaws.com/sentinel-cogs/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/S2A_36QWD_20200701_0_L2A.json -const sourceExtent = [499980, 1790220, 609780, 1900020]; +const source = new GeoTIFF({ + sources: [ + { + url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/TCI.tif', + }, + ], +}); const map = new Map({ target: 'map', layers: [ new TileLayer({ - source: new GeoTIFF({ - sources: [ - { - url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/TCI.tif', - }, - ], - }), - extent: sourceExtent, + source: source, }), ], - view: new View({ - projection: 'EPSG:32636', - center: getCenter(sourceExtent), - extent: sourceExtent, - zoom: 9, - }), + view: source.getView(), }); diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 037db533d2..3572686a66 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -137,7 +137,7 @@ import {removeNode} from './dom.js'; * element itself or the `id` of the element. If not specified at construction * time, {@link module:ol/Map~Map#setTarget} must be called for the map to be * rendered. If passed by element, the container can be in a secondary document. - * @property {View} [view] The map's view. No layer sources will be + * @property {View|Promise} [view] The map's view. No layer sources will be * fetched unless this is specified at construction time or through * {@link module:ol/Map~Map#setView}. */ @@ -388,6 +388,13 @@ class PluggableMap extends BaseObject { // is "defined" already. this.setProperties(optionsInternal.values); + const map = this; + if (options.view && !(options.view instanceof View)) { + options.view.then(function (viewOptions) { + map.setView(new View(viewOptions)); + }); + } + this.controls.addEventListener( CollectionEventType.ADD, /** @@ -1496,12 +1503,24 @@ class PluggableMap extends BaseObject { /** * Set the view for this map. - * @param {View} view The view that controls this map. + * @param {View|Promise} view The view that controls this map. + * It is also possible to pass a promise that resolves to options for constructing a view. This + * alternative allows view properties to be resolved by sources or other components that load + * view-related metadata. * @observable * @api */ setView(view) { - this.set(MapProperty.VIEW, view); + if (!view || view instanceof View) { + this.set(MapProperty.VIEW, view); + return; + } + this.set(MapProperty.VIEW, new View()); + + const map = this; + view.then(function (viewOptions) { + map.setView(new View(viewOptions)); + }); } /** @@ -1600,7 +1619,7 @@ function createOptionsInternal(options) { values[MapProperty.TARGET] = options.target; values[MapProperty.VIEW] = - options.view !== undefined ? options.view : new View(); + options.view instanceof View ? options.view : new View(); let controls; if (options.controls !== undefined) { diff --git a/src/ol/source/GeoTIFF.js b/src/ol/source/GeoTIFF.js index 0ebe8b436a..2ddf191018 100644 --- a/src/ol/source/GeoTIFF.js +++ b/src/ol/source/GeoTIFF.js @@ -5,10 +5,15 @@ import DataTile from './DataTile.js'; import State from './State.js'; import TileGrid from '../tilegrid/TileGrid.js'; import {Pool, fromUrl as tiffFromUrl, fromUrls as tiffFromUrls} from 'geotiff'; -import {Projection, get as getCachedProjection} from '../proj.js'; +import { + Projection, + get as getCachedProjection, + toUserCoordinate, + toUserExtent, +} from '../proj.js'; import {clamp} from '../math.js'; import {create as createDecoderWorker} from '../worker/geotiff-decoder.js'; -import {getIntersection} from '../extent.js'; +import {getCenter, getIntersection} from '../extent.js'; import {toSize} from '../size.js'; import {fromCode as unitsFromCode} from '../proj/Units.js'; @@ -181,15 +186,18 @@ function getImagesForSource(source) { * @param {number|Array|Array>} got Actual value. * @param {number} tolerance Accepted tolerance in fraction of expected between expected and got. * @param {string} message The error message. + * @param {function(Error):void} rejector A function to be called with any error. */ -function assertEqual(expected, got, tolerance, message) { +function assertEqual(expected, got, tolerance, message, rejector) { if (Array.isArray(expected)) { const length = expected.length; if (!Array.isArray(got) || length != got.length) { - throw new Error(message); + const error = new Error(message); + rejector(error); + throw error; } for (let i = 0; i < length; ++i) { - assertEqual(expected[i], got[i], tolerance, message); + assertEqual(expected[i], got[i], tolerance, message, rejector); } return; } @@ -433,7 +441,7 @@ class GeoTIFFSource extends DataTile { origin = sourceOrigin; } else { const message = `Origin mismatch for source ${sourceIndex}, got [${sourceOrigin}] but expected [${origin}]`; - assertEqual(origin, sourceOrigin, 0, message); + assertEqual(origin, sourceOrigin, 0, message, this.viewRejector); } if (!resolutions) { @@ -455,7 +463,8 @@ class GeoTIFFSource extends DataTile { resolutions.slice(minZoom, resolutions.length), scaledSourceResolutions, 0.005, - message + message, + this.viewRejector ); } @@ -466,7 +475,8 @@ class GeoTIFFSource extends DataTile { tileSizes.slice(minZoom, tileSizes.length), sourceTileSizes, 0, - `Tile size mismatch for source ${sourceIndex}` + `Tile size mismatch for source ${sourceIndex}`, + this.viewRejector ); } @@ -545,6 +555,13 @@ class GeoTIFFSource extends DataTile { this.setLoader(this.loadTile_.bind(this)); this.setState(State.READY); + this.viewResolver({ + projection: this.projection, + resolutions: resolutions, + center: toUserCoordinate(getCenter(extent), this.projection), + extent: toUserExtent(extent, this.projection), + zoom: 0, + }); } loadTile_(z, x, y) { @@ -650,4 +667,27 @@ class GeoTIFFSource extends DataTile { } } +/** + * Get a promise for view properties based on the source. Use the result of this function + * as the `view` option in a map constructor. + * + * const source = new GeoTIFF(options); + * + * const map = new Map({ + * target: 'map', + * layers: [ + * new TileLayer({ + * source: source, + * }), + * ], + * view: source.getView(), + * }); + * + * @function + * @return {Promise} A promise for view-related properties. + * @api + * + */ +GeoTIFFSource.prototype.getView; + export default GeoTIFFSource; diff --git a/src/ol/source/Source.js b/src/ol/source/Source.js index 8c3a5436ae..78ff0e2847 100644 --- a/src/ol/source/Source.js +++ b/src/ol/source/Source.js @@ -90,6 +90,28 @@ class Source extends BaseObject { * @type {boolean} */ this.wrapX_ = options.wrapX !== undefined ? options.wrapX : false; + + /** + * @protected + * @type {function(import("../View.js").ViewOptions):void} + */ + this.viewResolver = null; + + /** + * @protected + * @type {function(Error):void} + */ + this.viewRejector = null; + + const self = this; + /** + * @private + * @type {Promise} + */ + this.viewPromise_ = new Promise(function (resolve, reject) { + self.viewResolver = resolve; + self.viewRejector = reject; + }); } /** @@ -126,6 +148,13 @@ class Source extends BaseObject { return abstract(); } + /** + * @return {Promise} A promise for view-related properties. + */ + getView() { + return this.viewPromise_; + } + /** * Get the state of the source, see {@link module:ol/source/State~State} for possible states. * @return {import("./State.js").default} State. diff --git a/test/browser/spec/ol/map.test.js b/test/browser/spec/ol/Map.test.js similarity index 95% rename from test/browser/spec/ol/map.test.js rename to test/browser/spec/ol/Map.test.js index a2865e572d..c73d27a6cf 100644 --- a/test/browser/spec/ol/map.test.js +++ b/test/browser/spec/ol/Map.test.js @@ -33,13 +33,73 @@ import {createXYZ} from '../../../../src/ol/tilegrid.js'; import {defaults as defaultInteractions} from '../../../../src/ol/interaction.js'; import {tile as tileStrategy} from '../../../../src/ol/loadingstrategy.js'; -describe('ol.Map', function () { +describe('ol/Map', function () { describe('constructor', function () { it('creates a new map', function () { const map = new Map({}); expect(map).to.be.a(Map); }); + it('accepts a promise for view options', (done) => { + let resolve; + + const map = new Map({ + view: new Promise((r) => { + resolve = r; + }), + }); + + expect(map.getView()).to.be.a(View); + expect(map.getView().isDef()).to.be(false); + + map.once('change:view', () => { + const view = map.getView(); + expect(view).to.be.a(View); + expect(view.isDef()).to.be(true); + expect(view.getCenter()).to.eql([1, 2]); + expect(view.getZoom()).to.be(3); + done(); + }); + + resolve({ + center: [1, 2], + zoom: 3, + }); + }); + + it('allows the view to be set with a promise later after construction', (done) => { + const map = new Map({ + view: new View({zoom: 1, center: [0, 0]}), + }); + + expect(map.getView()).to.be.a(View); + expect(map.getView().isDef()).to.be(true); + + let resolve; + map.setView( + new Promise((r) => { + resolve = r; + }) + ); + + expect(map.getView()).to.be.a(View); + expect(map.getView().isDef()).to.be(false); + + map.once('change:view', () => { + const view = map.getView(); + expect(view).to.be.a(View); + expect(view.isDef()).to.be(true); + expect(view.getCenter()).to.eql([1, 2]); + expect(view.getZoom()).to.be(3); + done(); + }); + + resolve({ + center: [1, 2], + zoom: 3, + }); + }); + it('creates a set of default interactions', function () { const map = new Map({}); const interactions = map.getInteractions(); diff --git a/test/browser/spec/ol/source/geotiff.test.js b/test/browser/spec/ol/source/geotiff.test.js index c6cd9354d1..588c1b88a8 100644 --- a/test/browser/spec/ol/source/geotiff.test.js +++ b/test/browser/spec/ol/source/geotiff.test.js @@ -60,6 +60,18 @@ describe('ol/source/GeoTIFF', function () { }); }); + it('resolves view properties', function (done) { + source.getView().then((viewOptions) => { + const projection = viewOptions.projection; + expect(projection.getCode()).to.be('EPSG:4326'); + expect(projection.getUnits()).to.be('degrees'); + expect(viewOptions.extent).to.eql([-180, -90, 180, 90]); + expect(viewOptions.center).to.eql([0, 0]); + expect(viewOptions.resolutions).to.eql([0.703125]); + done(); + }); + }); + it('loads tiles', function (done) { source.on('change', () => { const tile = source.getTile(0, 0, 0);