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 = [
{