Allow maps to be configured with a promise for view props

This commit is contained in:
Tim Schaub
2021-09-21 00:06:26 +00:00
parent 79a54e33bb
commit d5813deb08
9 changed files with 230 additions and 116 deletions

View File

@@ -1,7 +1,6 @@
import GeoTIFF from '../src/ol/source/GeoTIFF.js'; import GeoTIFF from '../src/ol/source/GeoTIFF.js';
import Map from '../src/ol/Map.js'; import Map from '../src/ol/Map.js';
import TileLayer from '../src/ol/layer/WebGLTile.js'; import TileLayer from '../src/ol/layer/WebGLTile.js';
import View from '../src/ol/View.js';
const source = new GeoTIFF({ const source = new GeoTIFF({
sources: [ sources: [
@@ -57,9 +56,5 @@ const map = new Map({
source, source,
}), }),
], ],
view: new View({ view: source.getView(),
center: [1900000, 6100000],
zoom: 13,
minZoom: 10,
}),
}); });

View File

@@ -1,16 +1,21 @@
import GeoTIFF from '../src/ol/source/GeoTIFF.js'; import GeoTIFF from '../src/ol/source/GeoTIFF.js';
import Map from '../src/ol/Map.js'; import Map from '../src/ol/Map.js';
import TileLayer from '../src/ol/layer/WebGLTile.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'); const source = new GeoTIFF({
register(proj4); sources: [
{
// 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 // visible red, band 1 in the style expression above
const sourceExtent = [499980, 1790220, 609780, 1900020]; 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({ const map = new Map({
target: 'map', target: 'map',
@@ -69,27 +74,8 @@ const map = new Map({
[0, 69, 0], [0, 69, 0],
], ],
}, },
source: new GeoTIFF({ source: source,
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,
}), }),
], ],
view: new View({ view: source.getView(),
projection: 'EPSG:32636',
center: getCenter(sourceExtent),
extent: sourceExtent,
zoom: 9,
}),
}); });

View File

@@ -1,62 +1,49 @@
import GeoTIFF from '../src/ol/source/GeoTIFF.js'; import GeoTIFF from '../src/ol/source/GeoTIFF.js';
import Map from '../src/ol/Map.js'; import Map from '../src/ol/Map.js';
import TileLayer from '../src/ol/layer/WebGLTile.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 // scale values in this range to 0 - 1
const min = 10000; const min = 10000;
const max = 15000; 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({ const map = new Map({
target: 'map', target: 'map',
layers: [ layers: [
new TileLayer({ new TileLayer({
extent: sourceExtent,
style: { style: {
saturation: -0.3, saturation: -0.3,
}, },
source: new GeoTIFF({ source: source,
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,
},
],
}),
}), }),
], ],
view: new View({ view: source.getView(),
projection: 'EPSG:32645',
center: getCenter(sourceExtent),
extent: sourceExtent,
zoom: 8,
}),
}); });

View File

@@ -1,35 +1,21 @@
import GeoTIFF from '../src/ol/source/GeoTIFF.js'; import GeoTIFF from '../src/ol/source/GeoTIFF.js';
import Map from '../src/ol/Map.js'; import Map from '../src/ol/Map.js';
import TileLayer from '../src/ol/layer/WebGLTile.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'); const source = new GeoTIFF({
register(proj4); sources: [
{
// 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 url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/TCI.tif',
const sourceExtent = [499980, 1790220, 609780, 1900020]; },
],
});
const map = new Map({ const map = new Map({
target: 'map', target: 'map',
layers: [ layers: [
new TileLayer({ new TileLayer({
source: new GeoTIFF({ source: source,
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,
}), }),
], ],
view: new View({ view: source.getView(),
projection: 'EPSG:32636',
center: getCenter(sourceExtent),
extent: sourceExtent,
zoom: 9,
}),
}); });

View File

@@ -137,7 +137,7 @@ import {removeNode} from './dom.js';
* element itself or the `id` of the element. If not specified at construction * 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 * 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. * 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<import("./View.js").ViewOptions>} [view] The map's view. No layer sources will be
* fetched unless this is specified at construction time or through * fetched unless this is specified at construction time or through
* {@link module:ol/Map~Map#setView}. * {@link module:ol/Map~Map#setView}.
*/ */
@@ -388,6 +388,13 @@ class PluggableMap extends BaseObject {
// is "defined" already. // is "defined" already.
this.setProperties(optionsInternal.values); 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( this.controls.addEventListener(
CollectionEventType.ADD, CollectionEventType.ADD,
/** /**
@@ -1496,12 +1503,24 @@ class PluggableMap extends BaseObject {
/** /**
* Set the view for this map. * Set the view for this map.
* @param {View} view The view that controls this map. * @param {View|Promise<import("./View.js").ViewOptions>} 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 * @observable
* @api * @api
*/ */
setView(view) { 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.TARGET] = options.target;
values[MapProperty.VIEW] = values[MapProperty.VIEW] =
options.view !== undefined ? options.view : new View(); options.view instanceof View ? options.view : new View();
let controls; let controls;
if (options.controls !== undefined) { if (options.controls !== undefined) {

View File

@@ -5,10 +5,15 @@ import DataTile from './DataTile.js';
import State from './State.js'; import State from './State.js';
import TileGrid from '../tilegrid/TileGrid.js'; import TileGrid from '../tilegrid/TileGrid.js';
import {Pool, fromUrl as tiffFromUrl, fromUrls as tiffFromUrls} from 'geotiff'; 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 {clamp} from '../math.js';
import {create as createDecoderWorker} from '../worker/geotiff-decoder.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 {toSize} from '../size.js';
import {fromCode as unitsFromCode} from '../proj/Units.js'; import {fromCode as unitsFromCode} from '../proj/Units.js';
@@ -181,15 +186,18 @@ function getImagesForSource(source) {
* @param {number|Array<number>|Array<Array<number>>} got Actual value. * @param {number|Array<number>|Array<Array<number>>} got Actual value.
* @param {number} tolerance Accepted tolerance in fraction of expected between expected and got. * @param {number} tolerance Accepted tolerance in fraction of expected between expected and got.
* @param {string} message The error message. * @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)) { if (Array.isArray(expected)) {
const length = expected.length; const length = expected.length;
if (!Array.isArray(got) || length != got.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) { for (let i = 0; i < length; ++i) {
assertEqual(expected[i], got[i], tolerance, message); assertEqual(expected[i], got[i], tolerance, message, rejector);
} }
return; return;
} }
@@ -433,7 +441,7 @@ class GeoTIFFSource extends DataTile {
origin = sourceOrigin; origin = sourceOrigin;
} else { } else {
const message = `Origin mismatch for source ${sourceIndex}, got [${sourceOrigin}] but expected [${origin}]`; 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) { if (!resolutions) {
@@ -455,7 +463,8 @@ class GeoTIFFSource extends DataTile {
resolutions.slice(minZoom, resolutions.length), resolutions.slice(minZoom, resolutions.length),
scaledSourceResolutions, scaledSourceResolutions,
0.005, 0.005,
message message,
this.viewRejector
); );
} }
@@ -466,7 +475,8 @@ class GeoTIFFSource extends DataTile {
tileSizes.slice(minZoom, tileSizes.length), tileSizes.slice(minZoom, tileSizes.length),
sourceTileSizes, sourceTileSizes,
0, 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.setLoader(this.loadTile_.bind(this));
this.setState(State.READY); 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) { 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<import("../View.js").ViewOptions>} A promise for view-related properties.
* @api
*
*/
GeoTIFFSource.prototype.getView;
export default GeoTIFFSource; export default GeoTIFFSource;

View File

@@ -90,6 +90,28 @@ class Source extends BaseObject {
* @type {boolean} * @type {boolean}
*/ */
this.wrapX_ = options.wrapX !== undefined ? options.wrapX : false; 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<import("../View.js").ViewOptions>}
*/
this.viewPromise_ = new Promise(function (resolve, reject) {
self.viewResolver = resolve;
self.viewRejector = reject;
});
} }
/** /**
@@ -126,6 +148,13 @@ class Source extends BaseObject {
return abstract(); return abstract();
} }
/**
* @return {Promise<import("../View.js").ViewOptions>} 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. * Get the state of the source, see {@link module:ol/source/State~State} for possible states.
* @return {import("./State.js").default} State. * @return {import("./State.js").default} State.

View File

@@ -33,13 +33,73 @@ import {createXYZ} from '../../../../src/ol/tilegrid.js';
import {defaults as defaultInteractions} from '../../../../src/ol/interaction.js'; import {defaults as defaultInteractions} from '../../../../src/ol/interaction.js';
import {tile as tileStrategy} from '../../../../src/ol/loadingstrategy.js'; import {tile as tileStrategy} from '../../../../src/ol/loadingstrategy.js';
describe('ol.Map', function () { describe('ol/Map', function () {
describe('constructor', function () { describe('constructor', function () {
it('creates a new map', function () { it('creates a new map', function () {
const map = new Map({}); const map = new Map({});
expect(map).to.be.a(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 () { it('creates a set of default interactions', function () {
const map = new Map({}); const map = new Map({});
const interactions = map.getInteractions(); const interactions = map.getInteractions();

View File

@@ -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) { it('loads tiles', function (done) {
source.on('change', () => { source.on('change', () => {
const tile = source.getTile(0, 0, 0); const tile = source.getTile(0, 0, 0);