diff --git a/examples/vector-wfs-geographic.html b/examples/vector-wfs-geographic.html new file mode 100644 index 0000000000..f7acd5b60b --- /dev/null +++ b/examples/vector-wfs-geographic.html @@ -0,0 +1,16 @@ +--- +layout: example.html +title: WFS with geographic coordinates +shortdesc: Example of using WFS with a Tile strategy. +docs: > + This example loads new features from GeoServer WFS with a tile based loading strategy. + Calling the useGeographic function in the 'ol/proj' module + makes it so the map view uses geographic coordinates (even if the view projection is + not geographic). +tags: "geographic, vector, WFS, tile, strategy, loading, server, maptiler" +cloak: + - key: get_your_own_D6rA4zTHduk6KOKTXzGB + value: Get your own API key at https://www.maptiler.com/cloud/ +experimental: true +--- +
diff --git a/examples/vector-wfs-geographic.js b/examples/vector-wfs-geographic.js new file mode 100644 index 0000000000..dcc3ca8a6b --- /dev/null +++ b/examples/vector-wfs-geographic.js @@ -0,0 +1,60 @@ +import GeoJSON from '../src/ol/format/GeoJSON.js'; +import Map from '../src/ol/Map.js'; +import VectorSource from '../src/ol/source/Vector.js'; +import View from '../src/ol/View.js'; +import XYZ from '../src/ol/source/XYZ.js'; +import {Stroke, Style} from '../src/ol/style.js'; +import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; +import {createXYZ} from '../src/ol/tilegrid.js'; +import {tile} from '../src/ol/loadingstrategy.js'; +import {useGeographic} from '../src/ol/proj.js'; + +useGeographic(); + +const vectorSource = new VectorSource({ + format: new GeoJSON(), + url: function (extent) { + return ( + 'https://ahocevar.com/geoserver/wfs?service=WFS&' + + 'version=1.1.0&request=GetFeature&typename=osm:water_areas&' + + 'outputFormat=application/json&srsname=EPSG:4326&' + + 'bbox=' + + extent.join(',') + + ',EPSG:4326' + ); + }, + strategy: tile(createXYZ({tileSize: 512})), +}); + +const vector = new VectorLayer({ + source: vectorSource, + style: new Style({ + stroke: new Stroke({ + color: 'rgba(0, 0, 255, 1.0)', + width: 2, + }), + }), +}); + +const key = 'get_your_own_D6rA4zTHduk6KOKTXzGB'; +const attributions = + '© MapTiler ' + + '© OpenStreetMap contributors'; + +const raster = new TileLayer({ + source: new XYZ({ + attributions: attributions, + url: 'https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=' + key, + maxZoom: 20, + }), +}); + +const map = new Map({ + layers: [raster, vector], + target: document.getElementById('map'), + view: new View({ + center: [-80.0298, 43.4578], + maxZoom: 19, + zoom: 12, + }), +}); diff --git a/src/ol/loadingstrategy.js b/src/ol/loadingstrategy.js index bee3ac26a5..e27301e895 100644 --- a/src/ol/loadingstrategy.js +++ b/src/ol/loadingstrategy.js @@ -2,6 +2,8 @@ * @module ol/loadingstrategy */ +import {fromUserExtent, fromUserResolution, toUserExtent} from './proj.js'; + /** * Strategy function for loading all features with a single request. * @param {import("./extent.js").Extent} extent Extent. @@ -28,7 +30,7 @@ export function bbox(extent, resolution) { /** * Creates a strategy function for loading features based on a tile grid. * @param {import("./tilegrid/TileGrid.js").default} tileGrid Tile grid. - * @return {function(import("./extent.js").Extent, number): Array} Loading strategy. + * @return {function(import("./extent.js").Extent, number, import("./proj.js").Projection): Array} Loading strategy. * @api */ export function tile(tileGrid) { @@ -36,11 +38,17 @@ export function tile(tileGrid) { /** * @param {import("./extent.js").Extent} extent Extent. * @param {number} resolution Resolution. + * @param {import("./proj.js").Projection} projection Projection. * @return {Array} Extents. */ - function (extent, resolution) { - const z = tileGrid.getZForResolution(resolution); - const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z); + function (extent, resolution, projection) { + const z = tileGrid.getZForResolution( + fromUserResolution(resolution, projection) + ); + const tileRange = tileGrid.getTileRangeForExtentAndZ( + fromUserExtent(extent, projection), + z + ); /** @type {Array} */ const extents = []; /** @type {import("./tilecoord.js").TileCoord} */ @@ -55,7 +63,9 @@ export function tile(tileGrid) { tileCoord[2] <= tileRange.maxY; ++tileCoord[2] ) { - extents.push(tileGrid.getTileCoordExtent(tileCoord)); + extents.push( + toUserExtent(tileGrid.getTileCoordExtent(tileCoord), projection) + ); } } return extents; diff --git a/src/ol/proj.js b/src/ol/proj.js index 2b39fc9103..883f464783 100644 --- a/src/ol/proj.js +++ b/src/ol/proj.js @@ -631,6 +631,44 @@ export function fromUserExtent(extent, destProjection) { return transformExtent(extent, userProjection, destProjection); } +/** + * Return the resolution in user projection units per pixel. If no user projection + * is set, or source or user projection are missing units, the original resolution + * is returned. + * @param {number} resolution Resolution in input projection units per pixel. + * @param {ProjectionLike} sourceProjection The input projection. + * @return {number} Resolution in user projection units per pixel. + */ +export function toUserResolution(resolution, sourceProjection) { + if (!userProjection) { + return resolution; + } + const sourceUnits = get(sourceProjection).getUnits(); + const userUnits = userProjection.getUnits(); + return sourceUnits && userUnits + ? (resolution * METERS_PER_UNIT[sourceUnits]) / METERS_PER_UNIT[userUnits] + : resolution; +} + +/** + * Return the resolution in user projection units per pixel. If no user projection + * is set, or source or user projection are missing units, the original resolution + * is returned. + * @param {number} resolution Resolution in user projection units per pixel. + * @param {ProjectionLike} destProjection The destination projection. + * @return {number} Resolution in destination projection units per pixel. + */ +export function fromUserResolution(resolution, destProjection) { + if (!userProjection) { + return resolution; + } + const sourceUnits = get(destProjection).getUnits(); + const userUnits = userProjection.getUnits(); + return sourceUnits && userUnits + ? (resolution * METERS_PER_UNIT[userUnits]) / METERS_PER_UNIT[sourceUnits] + : resolution; +} + /** * Creates a safe coordinate transform function from a coordinate transform function. * "Safe" means that it can handle wrapping of x-coordinates for global projections, diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index da0de9da8b..65e161bd93 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -36,6 +36,7 @@ import { getTransformFromProjections, getUserProjection, toUserExtent, + toUserResolution, } from '../../proj.js'; import {getUid} from '../../util.js'; import {wrapX as wrapCoordinateX} from '../../coordinate.js'; @@ -642,9 +643,11 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { let userTransform; if (userProjection) { for (let i = 0, ii = loadExtents.length; i < ii; ++i) { + const extent = loadExtents[i]; + const userExtent = toUserExtent(extent, projection); vectorSource.loadFeatures( - toUserExtent(loadExtents[i], projection), - resolution, + userExtent, + toUserResolution(resolution, projection), userProjection ); } diff --git a/src/ol/source/Vector.js b/src/ol/source/Vector.js index 2ef5c3e0cb..5ce5f70029 100644 --- a/src/ol/source/Vector.js +++ b/src/ol/source/Vector.js @@ -26,7 +26,7 @@ import {xhr} from '../featureloader.js'; * returns an array of {@link module:ol/extent~Extent} with the extents to load. Usually this * is one of the standard {@link module:ol/loadingstrategy} strategies. * - * @typedef {function(import("../extent.js").Extent, number): Array} LoadingStrategy + * @typedef {function(import("../extent.js").Extent, number, import("../proj/Projection.js").default): Array} LoadingStrategy * @api */ @@ -939,7 +939,7 @@ class VectorSource extends Source { */ loadFeatures(extent, resolution, projection) { const loadedExtentsRtree = this.loadedExtentsRtree_; - const extentsToLoad = this.strategy_(extent, resolution); + const extentsToLoad = this.strategy_(extent, resolution, projection); for (let i = 0, ii = extentsToLoad.length; i < ii; ++i) { const extentToLoad = extentsToLoad[i]; const alreadyLoaded = loadedExtentsRtree.forEachInExtent( diff --git a/test/browser/spec/ol/loadingstrategy.test.js b/test/browser/spec/ol/loadingstrategy.test.js new file mode 100644 index 0000000000..0b734e60f4 --- /dev/null +++ b/test/browser/spec/ol/loadingstrategy.test.js @@ -0,0 +1,38 @@ +import {approximatelyEquals} from '../../../../src/ol/extent.js'; +import { + clearUserProjection, + get, + toUserExtent, + toUserResolution, + transformExtent, + useGeographic, +} from '../../../../src/ol/proj.js'; +import {createXYZ} from '../../../../src/ol/tilegrid.js'; +import {tile} from '../../../../src/ol/loadingstrategy.js'; + +describe('ol/loadingstrategy', function () { + describe('tile', function () { + afterEach(function () { + clearUserProjection(); + }); + it('uses a tile grid in view projection', function () { + useGeographic(); + const tileGrid = createXYZ(); + const strategy = tile(tileGrid); + const extent = tileGrid.getTileCoordExtent([1, 1, 1]); + const userExtent = toUserExtent(extent, get('EPSG:3857')); + const userResolution = toUserResolution( + tileGrid.getResolution(1), + get('EPSG:3857') + ); + const extents = strategy(userExtent, userResolution, get('EPSG:3857')); + expect( + approximatelyEquals( + transformExtent(extents[0], 'EPSG:4326', 'EPSG:3857'), + extent, + 1e-8 + ) + ).to.be(true); + }); + }); +}); diff --git a/test/node/ol/proj.test.js b/test/node/ol/proj.test.js index b2a723b233..5002b807b7 100644 --- a/test/node/ol/proj.test.js +++ b/test/node/ol/proj.test.js @@ -12,6 +12,7 @@ import { fromLonLat, fromUserCoordinate, fromUserExtent, + fromUserResolution, getPointResolution, get as getProjection, getTransform, @@ -21,6 +22,7 @@ import { toLonLat, toUserCoordinate, toUserExtent, + toUserResolution, transform, transformExtent, useGeographic, @@ -151,6 +153,36 @@ describe('ol/proj.js', function () { }); }); + describe('fromUserResolution()', function () { + it("adjusts a resolution for the user projection's units", function () { + useGeographic(); + const user = 1 / METERS_PER_UNIT['degrees']; + const resolution = fromUserResolution(user, 'EPSG:3857'); + expect(resolution).to.roughlyEqual(1, 1e-9); + }); + + it('returns the original if no user projection is set', function () { + const user = METERS_PER_UNIT['meters']; + const resolution = fromUserResolution(user, 'EPSG:3857'); + expect(resolution).to.eql(user); + }); + }); + + describe('toUserResolution()', function () { + it("adjusts a resolution for the user projection's units", function () { + useGeographic(); + const dest = 1; + const resolution = toUserResolution(dest, 'EPSG:3857'); + expect(resolution).to.eql(1 / METERS_PER_UNIT['degrees']); + }); + + it('returns the original if no user projection is set', function () { + const dest = METERS_PER_UNIT['degrees']; + const resolution = toUserResolution(dest, 'EPSG:3857'); + expect(resolution).to.eql(dest); + }); + }); + describe('toLonLat()', function () { const cases = [ {